All about the Magento 2 configuration settings fallback process

All about the Magento 2 configuration settings fallback process

Everyone knows about creating & assigning configuration settings in Magento 2. But do you know about the fallback process, and how values can be overridden per environment?

The configuration workflow in Magento 2 is pretty amazing, because it can be controlled globally, per domain, per site, or even down to a specific environment. This fallback process is generally unknown though, and becoming familiar with it can help you out when crafting our solutions for your clients, or architecting that custom Magento module for distribution.

Basic global configuration

Let's start by taking a look at an example of a typical basic global configuration. Assume we are trying to implement a "dark mode" feature on the frontend of our website, and we'd like a configuration setting to control this feature globally on our site.

We'd start by creating a module, if we don't already have one. Then we'd create a simple configuration definition within an adminhtml/system.xml file:

app/code/Acme/DarkMode/etc/adminhtml/system.xml
<?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>
        <!-- We target the "web" section, so we don't need to create a new section. -->
        <section id="web">
            <!--
            Note that we need set the showInDefault="1" attribute for this configuration option to
            show in the Default configuration, and setting "sortOrder" to a high number is
            recommended so we don't clash with core configuration settings.
            -->
            <group id="darkmode" translate="label" type="text" showInDefault="1" sortOrder="1000">
                <label>Dark Mode</label>
                <!-- The showInDefault="1" attribute also needs to be set here... -->
                <field id="enable" translate="label" type="select" showInDefault="1">
                    <label>Enable?</label>
                    <!-- The Yesno source model gives us that fancy Yes/No dropdown. -->
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
            </group>
        </section>
    </system>
</config>

This will create a new "Dark Mode" group at Stores > Configuration > General > Web, with an "Enable?" Yes/No dropdown:

Dark Mode adminhtml

That is all that is needed to create a new "system config" key and value!

It's important to note where exactly in the XML tree we are placing this configuration option. A great practice to follow is avoiding creating additional admin groups & sections if at all possible, so you can retain a clean admin interface. Since the General > Web section already exists, this is a great place to collate our custom configuration option.

We can convert the nested nodes into a path to reference our configuration option. Naturally, the path for this Dark Mode configuration can be referenced with:

web/darkmode/enable

You can break out of this default location by defining a config_path node containing the full path location of where you wish to store this config.

For example, if we wish to store this value at acme/darkmode/enable rather than web/darkmode/enable, we can add a node as a child to the field node with the contents: <config_path>acme/darkmode/enable</config_path>

We have not yet set a default value for this configuration. We can do this by creating a new config.xml file within our module, and reference the path of our new configuration property:

app/code/Acme/DarkMode/etc/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">
    <!-- This is the "config type" and is "default", "stores" or "websites" -->
    <default>
        <!-- web/darkmode/enable is the path for our configuration option. -->
        <web>
            <darkmode>
                <enable>0</enable>
                <!-- This tells web/darkmode/enable to default to 0 (No). -->
            </darkmode>
        </web>
    </default>
</config>

Since a configuration property has been defined and a default value set, we can now reference this configuration programmatically.

A quick way to check this would be using n98magerun2's dev:console command:

$di->get('Magento\Framework\App\Config\ScopeConfigInterface')->getValue('web/darkmode')

...which outputs:

=> [
     "enable" => "0",
   ]

If you are using docker-magento for your development environment, you can also use bin/devconsole as a shortcut for the n98magerun2 dev:console command.

Configuration scope for websites and stores

The setup we created above defines a "global" configuration property. We can also have configurations related to a specific website or store.

app/code/Acme/DarkMode/etc/adminhtml/system.xml
<?xml version="1.0"?>
<config ...>
    <system ...>
        <section ...>
            <!--
            We can also add attributes for showInWebsite="1" or showInStore="1"
            (or both) to have this property configurable within a website or
            store scope, respectively.
            -->
            <group id="darkmode" ... showInWebsite="1" showInStore="1">
            ...
                <field id="enable" ... showInWebsite="1" showInStore="1">
                ...

For now, Dark Mode is set to a global "No" value. However, the additions above now allow Dark Mode to be configured from the admin after selecting a scope other than "Default":

Admin scope

If we wished to change the default configuration value for a specific scope, we can also add a secondary value to our module's config.xml file:

app/code/Acme/DarkMode/etc/config.xml
<?xml version="1.0"?>
<config ...>
    <default>
        ...
    </default>
    <!-- We can also use "websites" or "stores" here to target a specific scope. -->
    <websites>
        <base>
            <!-- The config here will only apply to the "base" website -->
            <web>
                <darkmode>
                    <enable>1</enable>
                    <!--
                    This tells web/darkmode/enable to default to 1 (Yes), but just
                    for this specific website.
 
                    Since default values are not defined for other websites, the
                    web/darkmode/enable value will fall back to the "default" scope
                    for these sites, which is currently set to 0 (No).
                    -->
                </darkmode>
            </web>
        </base>
    </websites>
</config>

The code above targets the "base" website. You can find the code to use for this value by going to Admin > Stores > Settings > All Stores, and looking at the values from the "Web Site" column for the "code" to target.

Similarly, you can target a specific store by using the stores node in tandem with the related "code" from the "Store" column data.

Admin store codes

All of the config.xml files in Magento are merged into one giant XML tree. If you wish to override third-party config.xml files, you can use the <sequence> node in your module's module.xml file to control the load ordering of modules. This controls the precedence of how XML nodes are merged together.

The last value in an XML tree always has precedence, so make sure your module loads "after" others to take control of the value you are targeting.

Database configurations take precedence

As of right now, all configuration properties are using the default values. But if we change & save a configuration value from the admin, it will save the value to a new row in the core_config_data database table.

Dark mode enabled core_config_data

The last record in the core_config_data database table stores the path of web/darkmode/enable with the value of 1 set for the default scope.

Note that for websites and stores scopes, the database uses the scope_id with the numeric representation of the related scope.

You can find this numeric id in the store and store_website database tables.

Once the data is saved in the database, it can additionally be retrieved using the built-in bin/magento command with:

bin/magento config:show web/darkmode/enable

This command will return 1, which is the value retrieved from the core_config_data database table for this property. It will not a return a value if there are no related records that exist in this database table.

We can also set a value for this configuration property by using the config:set command:

bin/magento config:set web/darkmode/enable 0

Upon refreshing the contents of the core_config_data records, we'll see that the record for this property did indeed update:

core_config_data updated

We can use the --scope and --scope-code command parameters to define a scope other than "default".

For example, we can set a value for the "base" website with:

bin/magento config:set --scope=websites --scope-code=base web/darkmode/enable 1

As expected, this creates yet another database record in the core_config_data table for the scope that was passed in. Note that the scope_code is translated into the numeric representation for that code at the time of insertion:

core_config_data updated scope

Values in the core_config_data database table always override values defined in the PHP or XML layer. This provides a fallback mechanism, but there are additional fallback processes in place for environment management.

config.php overrules database configurations

Magento defines "shared-configuration" settings within the file at app/etc/config.php.

This file is committed to version control, and is shared among all environments. The configurations are stored in a multi-dimensional array under the system index in the format:

app/etc/config.php
return [
    ...
    'system' => [
        'websites' => [
            'base' => [
                'web' => [
                    'darkmode' => [
                        'enable' => '0',
                    ],
                ],
            ],
        ],
    ],
];

This example uses the websites scope, however you can also target default or stores scopes as well. Be sure to write all configuration values as either null or a string, even integer values such as the one above.

After adding or changing a value within this file, you'll notice an error on the frontend of your Magento site:

1 exception(s):
Exception #0 (Magento\Framework\Exception\LocalizedException): The configuration file has changed. Run the "app:config:import" or the "setup:upgrade" command to synchronize the configuration.

Magento detects mismatches between changes of value in your database compared to this file. The fix is easy though, just run:

bin/magento app:config:import

After importing these values, any code looking up the value for this configuration property will return the value as expected. However, it's important to know that the core_config_data table will not be updated with values from the import.

Again testing with n98magerun2 dev:console:

$di->get('Magento\Framework\App\Config\ScopeConfigInterface')->getValue('web/darkmode', 'websites', 'base')

...outputs:

=> [
     "enable" => "0",
   ]

There appears to be a bug with the bin/magento script when retrieving the value with a specific scope, as bin/magento config:show --scope=websites --scope-code=base web/darkmode/enable returns 1. This was found during the writing of this article, and a bug will be filed with the Magento 2 GitHub repo shortly.

Defining a value within the config.php file will lock the value from being edited in the admin:

Dark mode disabled

This is useful if you wish to hard-code a value for a specific configuration property and scope, and not allow it to be controlled by an admin user.

There's yet another level of precedence though over config.php files...

env.php overrules config.php

The app/etc/env.php file acts in an extremely similar manner to the app/etc/config.php file, using the same format:

app/etc/env.php
return [
    ...
    'system' => [
        'websites' => [
            'base' => [
                'web' => [
                    'darkmode' => [
                        'enable' => '1',
                    ],
                ],
            ],
        ],
    ],
];

There is a big difference between config.php and env.php though, and that is the env.php file is not committed to version control, and this file is not shared among environments.

This allows you to define environment-specific changes to a configuration property value, so different environments can have different values. Similarly to config.php, values defined in env.php files are locked and are not editable from the admin.

Environment variables have the final word

Just when you thought this article was over, there is yet one more precedence to the fallback of a Magento configuration, and that is the use of an environment variable. This functions just like the env.php file, but doesn't need to be set within this file. This can be useful with ephemeral, read-only servers, as well as some other specific cases.

Like everything else in Magento, setting a configuration value with an environment variable follows a very specific naming convention.

For default-scoped configurations:

CONFIG__DEFAULT__<CONFIGURATION__NAME>

...and for websites or stores scopes:

CONFIG__<WEBSITES_OR_STORES>__<SCOPE_CODE>__<CONFIGURATION__NAME>

For our Dark Mode configuration property, this would be set with:

CONFIG__WEBSITES__BASE__WEB__DARKMODE__ENABLE=1

You can read in the DevDocs about extended usage of environment variables to override configuration settings.

Conclusion

I hope you learned at least one new thing about the Magento configuration by reading this article.

If you liked it, please share it with your fellow Magento developers! You may also sign up below to get notified about new Magento-related blog posts.

Intrigued to learn more about Magento? Experience these 3 helpful things that can help you:

  1. Explore Magento 2 fundamentals & best practices course (1,000+ students)
  2. Grow your Magento expertise with all courses & lessons (700+ students)
  3. Learn visually with blocks of code & inline comments (3,000+ students)