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:
- Environment variables (
CONFIG__WEBSITES__BASE__WEB__DARKMODE__ENABLE=1) app/etc/env.php(per environment, not in version control)app/etc/config.php(shared, committed to version control)- The
core_config_datatable (what the admin andbin/magento config:setwrite to) etc/config.xmlin 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:
<?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:
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/enableYou can break out of this default location by defining a
config_pathnode 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/enablerather thanweb/darkmode/enable, we can add a node as a child to thefieldnode 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:
<?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/devconsoleas a shortcut for then98magerun2 dev:consolecommand. 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.
<?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":
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:
<?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.
All of the
config.xmlfiles in Magento are merged into one giant XML tree. If you wish to override third-partyconfig.xmlfiles, you can use the<sequence>node in your module'smodule.xmlfile 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.jsonrequiresection 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.
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
websitesandstoresscopes, the database uses thescope_idwith the numeric representation of the related scope.You can find this numeric id in the
storeandstore_websitedatabase 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/enableThis 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 0Upon refreshing the contents of the core_config_data records, we'll see that the record for this property did indeed update:
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 1As 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:
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:
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:importAfter 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:
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:
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 0Both 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=1Adobe'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.



