Magento 2

Most of the time during custom module development you will get to the point where some of your settings should be available for customization without adjustments to the codebase.

Fortunately, Magento is shipped with a feature that allows to easily display configuration fields in the administration panel. Those fields are automatically stored in the database upon saving and can be simply retrieved through a predefined interface. This feature is called System Configuration, and you will most likely see it also being called Store Configuration in some of the resources. Remember that both reference the same functionality.

What is System Configuration?

System Configuration is a feature intended for store administrators to help them manage how the Magento and third-party modules behave in the system. No technical knowledge or implementation details are required in order to adjust those settings.

You can access System Configuration by navigating to Stores -> Configuration in the administration panel menu:

Click to zoom

and here is what it looks like:

Click to zoom

Configuration is divided into tabs, sections, groups, and fields.

The relationship between those objects is following:

Click to zoom

Each section must be contained within a tab. (tab -> section)
Each group must be contained within a section. (section -> group)
Each field must be contained within a group. (group -> field)
So it goes like this: tab -> section -> group -> field

Programatically adding new tabs, sections, groups or fields goes a little bit different, and here is how to do it.

How to add your own System Configuration

First of all, you need to create a custom module.

If you don’t know what a custom module is, or how to create it, read this post.

We don’t need anything fancy here, just a barebone minimum will do (so registration.php and etc/module.xml).

Once you create your custom module, remember to enable it through bin/magento module:enable CLI command to let Magento know that core code should take your module into consideration.

Adjusting System Configuration in Magento is done through XML, namely by app/code/app/code/Vendor/Module/etc/adminhtml/system.xml file, so that’s what we are going to use.

Create system.xml file inside your module, and add this:

+ <?xml version="1.0"?>
+ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
+         xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
+    <system>
+    </system>
+ </config>

xmlns:xsi and xsi:noNamespaceSchemaLocation attributes are used all over Magento. I created a more in-depth explanation about them in this post.

For now, just remember that those fields are your friends, and shouldn’t be skipped. They help your IDE to auto-suggest what you should type in, and also help to validate what was already typed in, and if it matches the schemas.

<system> is a root tag for all of the changes related to System Configuration. Let’s proceed with adding a new tab, section, group, and a couple of fields there.

Example Module

Let’s say our module is going to integrate third-party API. We want to ensure that this integration can be enabled/disabled through system configuration. We need API Keys, and some way to validate them. API is also versioned and we must be able to select which version is used. Besides API configuration an optional notice can be displayed on the frontend after a certain hour passes.

User Story – System Configuration

As an administrator, I can manage module settings through a System Configuration in a separate tab called Vendor Extensions and through a new section called Third-Party Configuration

General configuration

Those fields must be in a separate group.

  • As an administrator, I can enable, and disable module.
  • As an administrator, I can save third-party API Key and API Secret in a secure way. No other administrator should be able to see it.
  • As an administrator, I can click on Validate API Keys button to check if both API Key and API Secret are valid.
  • As an administrator, I can select which version of third-party API will be used. The list of available choices is following: v1, v2, v3

Frontend configuration

Those fields must be in a separate group.

  • As an administrator I can enable/disable custom frontend notice.
  • As an administrator, I can input notice that will be displayed on the frontend. The input field should be tall enough to display 5 lines. The field is only visible when notice is enabled.
  • As an administrator I can schedule a time after which notice will be displayed. The field is only visible when notice is enabled.

System Configuration Scopes

Sections, groups, and fields are displayed per scope, and by default, elements are hidden. We have to enable it in the XML in order for our configuration to display correctly.

There are three configuration scopes that elements can be displayed in:

  • default
  • website
  • store

and each scope must be enabled separately for every element that we want to display.

Read more about scopes in Magento in this post.

You can use the following attributes to show configuration elements in the given scope:

for default scope use showInDefault="1"
for website scope use showInWebsite="1"
for store scope use showInStore="1"

For the sake of simplicity let’s assume that our Magento environment is running a single store mode, so there are no websites or stores for us to care about.

We have to adjust our configuration so it displays everything in default scope by using showInDefault="1" attribute

Adding new tab

 <?xml version="1.0"?>
 <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
     <system>
+         <tab id="vendor_extensions">
+             <label>Vendor Extensions</label>
+         </tab>
     </system>
 </config>

We can’t preview it yet. There must be at least one visible section, group, and field in order for the tab to display. Let’s add them.

Adding new section

Sections are added directly inside the <config> tag, even though they are displayed inside the tabs. You can connect sections and tabs together through the <tab> tag, where you put a tab id.

 <?xml version="1.0"?>
 <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
     <system>
         <tab id="vendor_extensions">
             <label>Vendor Extensions</label>
         </tab>
+        <section id="thirdparty_module">
+            <tab>vendor_extensions</tab>
+            <label>Third-Party Module</label>
+        </section>
     </system>
 </config>

Let’s also make it visible on the default scope:

 <?xml version="1.0"?>
 <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
     <system>
         <tab id="vendor_extensions">
             <label>Vendor Extensions</label>
         </tab>
-        <section id="thirdparty_module">
+        <section id="thirdparty_module" showInDefault="1">
             <tab>vendor_extensions</tab>
             <label>Third-Party Module</label>
         </section>
     </system>
 </config>

Now, let’s add two groups mentioned in the user story: General Configuration and Frontend Configuration

Adding new groups

Groups are always nested inside the <section> tags that they belong to:

 <?xml version="1.0"?>
 <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
      <system>
         <tab id="vendor_extensions">
             <label>Vendor Extensions</label>
         </tab>
        <section id="thirdparty_module">
            <tab>vendor_extensions</tab>
            <label>Third-Party Module</label>
+            <group id="general">
+                <label>General Configuration</label>
+            </group>
+            <group id="frontend">
+                <label>Frontend Configuration</label>
+            </group>
        </section>
     </system>
 </config>

And like we did with the section, let’s also ensure that groups are visible in the default scope:

 <?xml version="1.0"?>
 <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
      <system>
         <tab id="vendor_extensions">
             <label>Vendor Extensions</label>
         </tab>
        <section id="thirdparty_module">
            <tab>vendor_extensions</tab>
            <label>Third-Party Module</label>
-            <group id="general">
+            <group id="general" showInDefault="1">
                 <label>General Configuration</label>
             </group>
-            <group id="frontend">
+            <group id="frontend" showInDefault="1">
                 <label>Frontend Configuration</label>
             </group>
        </section>
     </system>
 </config>

All that’s left are fields.

Adding new fields

Adding new fields is a little bit complicated because the field can have various types, and those types determine how the field is displayed in the Store > Configuration.

I’m now going to safely walk you through adding new fields based on our custom module user story. It’s OK if you don’t understand what’s going on yet.

I covered all of the field types with an explanation of how they are displayed and work in this post.

General configuration

Those fields must be in a separate group.

As an administrator, I can enable, and disable module

One of the common types often used in system configuration is select. It’s very flexible and lets you specify options directly through XML (I put an example of that in one of the next fields) or through the source_model.

source_model is used to define option source through a PHP Class that implements \Magento\Framework\Data\OptionSourceInterface. Output array from this class is used to render <option> tags for a given select field. You can easily define your own options for select fields by creating a new class that implements this interface. All you have to do here is to add a new public method called toOptionArray() and return an array with label-value pairs:

<?php
declare(strict_types=1);

namespace Vendor\Module\Model\Config\Source;

class CustomOptionSource implements \Magento\Framework\Data\OptionSourceInterface
{
    public function toOptionArray() : array
    {
        return [
            [
                'label' => 'Foo',
                'value' => 'foo'
            ],
            [
                'label' => 'Bar',
                'value' => 'bar'
            ]
        ];
    }
}

Then, it can be used in the XML in the following way:

+ <source_model>Vendor\Module\Model\Config\Source\CustomOptionSource</source_model>

For our field, we will use already predefined Yes/No option source and I strongly recommend using it for any select fields that you are going to set in the future and are just plain yes/no. Here is what it looks like:

 ...
            <group id="general">
                <label>General Configuration</label>
+               <field id="enable" type="select">
+                   <label>Enable Module</label>
+                   <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
+               </field>
            </group>
 ...

And let’s make it visible on the default scope:

 ...
            <group id="general">
                <label>General Configuration</label>
-               <field id="enable" type="select">
+               <field id="enable" type="select" showInDefault="1">
                   <label>Enable Module</label>
                   <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
               </field>
            </group>
 ...

As an administrator, I can save third-party API Key and API Secret in a secure way. No other administrator should be able to see it.

There are two types that come in handy here: password and obscure. The difference between them is that password type is just plain <input type="password">, so values can be directly viewed by changing input’s type to type="text" in the DevTools. obscure in other way is also <input type="password"> but there is one key distinction. Value is hidden on the frontend, so once you change the input type to text through DevTools all you are going to see is ****. This is a convenient way to hide values from other administrators. Let’s use type obscure on both fields:

 ...
             <group id="general">
                 <label>General Configuration</label>
                 <field id="enable" type="select">
                     <label>Enable Module</label>
                     <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                 </field>
+                <field id="api_key" type="obscure">
+                    <label>API Key</label>
+                </field>
+                <field id="api_secret" type="obscure">
+                    <label>API Secret</label>
+                </field>
             </group>
 ...

Those values are still stored as plain text in the database and are not encrypted. We will secure it even further by using special backend_model.

backend_model is a PHP class that extends Magento\Framework\App\Config\Value, and it can be used to process raw value. This comes in handy if you want to process value into a new format (i.e JSON serializing) before it gets saved in the database.

Magento supports encrypting configuration values out of the box. In order to encrypt anything, we can use a pre-defined backend_model that is called Magento\Config\Model\Config\Backend\Encrypted

 ...
             <group id="general">
                 <label>General Configuration</label>
                 <field id="enable" type="select">
                     <label>Enable Module</label>
                     <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                 </field>
                <field id="api_key" type="obscure" showInDefault="1">
                    <label>API Key</label>
+                    <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
                </field>
                <field id="api_secret" type="obscure" showInDefault="1">
                    <label>API Secret</label>
+                    <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
                </field>
             </group>
 ...

As an administrator, I can click on Validate API Keys button to check if both API Key and API Secret are valid

 ...
             <group id="general">
                 <label>General Configuration</label>
                 <field id="enable" type="select">
                     <label>Enable Module</label>
                     <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                 </field>
                 <field id="api_key" type="password">
                     <label>API Key</label>
                 </field>
                 <field id="api_secret" type="password">
                     <label>API Secret</label>
                 </field>
+                <field id="validate_api_keys" type="button">
+                    <label>Validate API Keys</label>
+                </field>
             </group>
 ...

As an administrator, I can select which version of third-party API will be used

The list of available choices is the following: v1, v2, and v3.

 ...
             <group id="general">
                 <label>General Configuration</label>
                 <field id="enable" type="select">
                     <label>Enable Module</label>
                     <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                 </field>
                 <field id="api_key" type="password">
                     <label>API Key</label>
                 </field>
                 <field id="api_secret" type="password">
                     <label>API Secret</label>
                 </field>
                 <field id="validate_api_keys" type="button">
                     <label>Validate API Keys</label>
                 </field>
+                <field id="api_version" type="select">
+                    <label>API Version</label>
+                    <options>
+                        <option label="v1">v1</option>
+                        <option label="v2">v2</option>
+                        <option label="v3">v3</option>
+                    </options>
+                </field>
             </group>
 ...

Frontend configuration

Those fields must be in a separate group.

As an administrator I can enable/disable custom frontend notice

...
             <group id="frontend">
                 <label>Frontend Configuration</label>
+                <field id="enable_notice" type="select">
+                    <label>Enable Frontend Notice</label>
+                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
+                </field>
             </group>
...

As an administrator, I can input notice that will be displayed on the frontend

The input field should be tall enough to display 5 lines. The field is visible only when notice is enabled.

...
             <group id="frontend">
                 <label>Frontend Configuration</label>
                 <field id="enable_notice" type="select">
                     <label>Enable Frontend Notice</label>
                     <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                 </field>
+                <field id="notice" type="textarea">
+                    <label>Frontend Notice</label>
+                    <depends>
+                        <field id="enable_notice">1</field>
+                    </depends>
+                </field>
             </group>
...

As an administrator, I can schedule a time after which the notice will be displayed

The field is visible only when notice is enabled.

...
             <group id="frontend">
                 <label>Frontend Configuration</label>
                 <field id="enable_notice" type="select">
                     <label>Enable Frontend Notice</label>
                     <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                 </field>
                 <field id="notice" type="textarea">
                     <label>Frontend Notice</label>
                     <depends>
                         <field id="enable_notice">1</field>
                     </depends>
                 </field>
+                <field id="display_notice_after" type="time">
+                    <label>Display Notice After (Hour/Minute/Second)</label>
+                    <depends>
+                        <field id="enable_notice">1</field>
+                    </depends>
+                </field>
             </group>
...

Are we there yet?

No… it’s about to get more complicated.

 <?xml version="1.0"?>
 <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
     <system>
         <tab id="vendor_extensions">
             <label>Vendor Extensions</label>
         </tab>
-        <section id="thirdparty_module">
+        <section id="thirdparty_module" showInDefault="1">
             <tab>vendor_extensions</tab>
             <label>Third-Party Module</label>
-            <group id="general">
+            <group id="general" showInDefault="1">
                 <label>General Configuration</label>
-                <field id="enable" type="select">
+                <field id="enable" type="select" showInDefault="1">
                     <label>Enable Module</label>
                     <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                 </field>
-                <field id="api_key" type="password">
+                <field id="api_key" type="password" showInDefault="1">
                     <label>API Key</label>
                 </field>
-                <field id="api_secret" type="password">
+                <field id="api_secret" type="password" showInDefault="1">
                     <label>API Secret</label>
                 </field>
-                <field id="validate_api_keys" type="button">
+                <field id="validate_api_keys" type="button" showInDefault="1">
                     <label>Validate API Keys</label>
                 </field>
-                <field id="api_version" type="select">
+                <field id="api_version" type="select" showInDefault="1">
                     <label>API Version</label>
                     <options>
                         <option label="v1">v1</option>
                         <option label="v2">v2</option>
                         <option label="v3">v3</option>
                     </options>
                 </field>
             </group>
-            <group id="frontend">
+            <group id="frontend" showInDefault="1">
                 <label>Frontend Configuration</label>
-                <field id="enable_notice" type="select">
+                <field id="enable_notice" type="select" showInDefault="1">
                     <label>Enable Frontend Notice</label>
                     <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                 </field>
-                <field id="notice" type="textarea">
+                <field id="notice" type="textarea" showInDefault="1">
                     <label>Frontend Notice</label>
                     <depends>
                         <field id="enable_notice">1</field>
                     </depends>
                 </field>
-                <field id="display_notice_after" type="time">
+                <field id="display_notice_after" type="time" showInDefault="1">
                     <label>Display Notice After (Hour/Minute/Second)</label>
                     <depends>
                         <field id="enable_notice">1</field>
                     </depends>
                 </field>
             </group>
         </section>
     </system>
 </config>

Looks nice. And here is how you can retrieve it in PHP.

Retrieve configuration values in PHP

Magento provides an interface that you can use to retrieve configuration values from the database. It’s called \Magento\Framework\App\Config\ScopeConfigInterface. You need to inject it into the Config class in your module.

Here is what this class can look like:

<?php

declare(strict_types=1);

namespace PeterRusin\Cache\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;

class Config
{
    private const API_KEY_XML_PATH = 'thirdparty_module/general/api_key';
    private const API_SECRET_XML_PATH = 'thirdparty_module/general/api_secret';
    private const ENABLE_NOTICE_XML_PATH = 'thirdparty_module/frontend/enable_notice';
    private const NOTICE_XML_PATH = 'thirdparty_module/frontend/notice';
    private const DISPLAY_NOTICE_AFTER_XML_PATH = 'thirdparty_module/frontend/display_notice_after';

    /**
     * @var \Magento\Framework\App\Config\ScopeConfigInterface
     */
    private $config;

    public function __construct(ScopeConfigInterface $config)
    {
        $this->config = $config;
    }

    public function getApiKey(?int $storeId = null) : ?string
    {
        return $this->config->getValue(
            self::API_KEY_XML_PATH,
            ScopeInterface::SCOPE_STORE,
            $storeId
        );
    }

    public function getApiSecret(?int $storeId = null) : ?string
    {
        return $this->config->getValue(
            self::API_SECRET_XML_PATH,
            ScopeInterface::SCOPE_STORE,
            $storeId
        );
    }

    public function isNoticeEnabled(?int $storeId = null) : bool
    {
        return $this->config->isSetFlag(
            self::ENABLE_NOTICE_XML_PATH,
            ScopeInterface::SCOPE_STORE,
            $storeId
        );
    }

    public function getNotice(?int $storeId = null) : ?string
    {
        return $this->config->getValue(
            self::NOTICE_XML_PATH,
            ScopeInterface::SCOPE_STORE,
            $storeId
        );
    }

    public function getDisplayNoticeAfter(?int $storeId = null) : ?string
    {
        return $this->config->getValue(
            self::DISPLAY_NOTICE_AFTER_XML_PATH,
            ScopeInterface::SCOPE_STORE,
            $storeId
        );
    }
}

By default, Magento retrieves configuration values for the store you are currently in (determined based on domain name and environment variables) but it’s a good practice to add support for retrieving values from within any store that you want, so all of the functions include ?int $storeId = null.