Maggie 3.0Meet the newly updated AI tutor

See what's new
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. Fewer know the fallback process, and how values can be overridden per environment.

Magento resolves a config value from the strongest source down: environment variables (CONFIG__DEFAULT__...), then app/etc/env.php, then app/etc/config.php, then the core_config_data table, then each module's etc/config.xml. Within each source, store view beats website beats default. Values set in the first three are locked in the admin.

That's five places a value can come from, and the one you see in the admin is often not the one that wins. Here's the order, from strongest to weakest:

  1. Environment variables (CONFIG__WEBSITES__BASE__WEB__DARKMODE__ENABLE=1)
  2. app/etc/env.php (per environment, not in version control)
  3. app/etc/config.php (shared, committed to version control)
  4. The core_config_data table (what the admin and bin/magento config:set write to)
  5. etc/config.xml in each module (the defaults)

Let's build one config value from scratch and push it up through every level, starting from the bottom.

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. In real code, you inject Magento\Framework\App\Config\ScopeConfigInterface into your class's constructor and call getValue() on it.

A quick way to check this from the command line 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. And if you'd rather poke at config from a GUI, Tinkerwell works with Magento too.

Configuration scope for websites and stores

The setup we created above defines a configuration property at the "default" scope. Magento has three config scopes: default, website, and store view. We can also have configurations related to a specific website or store view.

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. A module you sequence after is a real dependency, so add it to your module's composer.json require section too.

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.

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 a module's config.xml. But there are three more levels above the database, and they exist 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",
   ]

bin/magento config:show --scope=websites --scope-code=base web/darkmode/enable reads from the database, config.php/env.php and environment variables together, so it reports the winning value too.

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.

Why can't I change this config value in the admin?

If a field is greyed out with a note that it's locked, the value is coming from config.php, env.php or an environment variable, and the admin is read-only for it. Saving in the admin, or running a plain config:set, can't override it. Find the value in one of those three places and change it there.

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.

Writing locked values with config:set

You don't need to hand-edit those PHP arrays. config:set can write straight to either file, with the same --scope and --scope-code options as before:

# Lock the value in app/etc/env.php (this environment only)
bin/magento config:set --lock-env --scope=websites --scope-code=base web/darkmode/enable 1
 
# Lock the value in app/etc/config.php (shared, committed to version control)
bin/magento config:set --lock-config --scope=websites --scope-code=base web/darkmode/enable 0

Both skip the database entirely. To go the other way and export what's already in core_config_data into these files, run bin/magento app:config:dump. Be careful with that one: with no arguments it dumps every system value into config.php (and sensitive ones into env.php), which locks all of them in the admin at once.

Environment variables have the final word

Environment variables sit above everything, including env.php. They function just like the env.php file, but don't need to be set within this file. This is useful with ephemeral, read-only servers and containers, 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

Adobe's docs cover extended usage of environment variables to override configuration settings.

Conclusion

Next time a value won't change from the admin, check environment variables, env.php and config.php before you touch the database. And remember that this same fallback applies to anything stored in config, including the <config_path> schedules that drive Magento cron jobs.

If you want to go deeper on system configuration and the rest of Magento's fundamentals, the Magento 2 Coding Kickstart course covers it.

Something here out of date?

Magento moves, and so does this site. If a step no longer works, tell me and I'll fix it.

Get the next one by email

Every other week I send a short newsletter on how PHP + Magento actually work: releases, security alerts, and one thing explained under the hood. Enroll in the Library and it's yours. No credit card needed.

The University includes every PHP + Magento course available, and every new M.academy course as it ships.