Module Configuration with config.xml

config.xml ships a module's default settings, and ScopeConfigInterface reads them back by section/group/field path.

Syntax$this->scopeConfig->getValue('section/group/field', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null)

Every setting in Magento is addressed by a config path: three segments written section/group/field. general/store_information/name is one. A module ships the starting value for its own paths in etc/config.xml, so the setting exists and has a sane value before anyone opens the admin.

How it works

config.xml is an XML tree whose node names are the path segments, wrapped in a <default> element. Magento merges the config.xml of every enabled module into one tree, then layers the database on top of it. Reading is always through Magento\Framework\App\Config\ScopeConfigInterface, injected in a constructor.

Two shorthands are worth recognising. getValue($path) is the same call as getValue($path, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null), because those are the declared defaults. And isSetFlag($path) is the same thing as !!getValue($path) — the framework implements it that way.

What trips people up

config.xml is a default, not a setting. The moment someone saves that field in the admin, a row appears in core_config_data and wins from then on. The file is also cached: after editing config.xml run bin/magento cache:flush config. A brand new module needs bin/magento setup:upgrade first, because a module Magento has not registered never has its config.xml read at all.

Example

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>1</enabled>
                <points_per_currency>10</points_per_currency>
            </general>
            <email>
                <sender_email_identity>support</sender_email_identity>
            </email>
        </loyalty>
    </default>
</config>

When to use it

  • Ship a working default for a new module setting so the storefront behaves before anyone opens the admin.
  • Read a configurable rate or limit in a controller instead of hardcoding the number.
  • Raise a core default, such as products per page, for every installation built from your repository.
  • Confirm from the command line which value the application is actually resolving for a path.

More examples

Reading the value in a controller

With the module's routes.xml declaring the frontName loyalty, a GET to /loyalty/points/rate answers 200 with the JSON body {"enabled":true,"rate":10,"same":true} straight from config.xml, because nothing has been saved in the admin yet. ScopeConfigInterface is an interface, so it is injected directly; nothing here needs a factory. Reach for this shape whenever a number or a toggle should be adjustable without a code change.

Example · php
<?php
// app/code/Acme/Loyalty/Controller/Points/Rate.php
declare(strict_types=1);

namespace Acme\Loyalty\Controller\Points;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Controller\Result\Json;
use Magento\Framework\Controller\Result\JsonFactory;

class Rate implements HttpGetActionInterface
{
    private const PATH_RATE = 'loyalty/general/points_per_currency';

    public function __construct(
        private readonly ScopeConfigInterface $scopeConfig,
        private readonly JsonFactory $resultJsonFactory
    ) {
    }

    public function execute(): Json
    {
        // Shorthand.
        $rate = $this->scopeConfig->getValue(self::PATH_RATE);

        // Longhand. Identical call: these are the declared defaults.
        $rateLong = $this->scopeConfig->getValue(
            self::PATH_RATE,
            ScopeConfigInterface::SCOPE_TYPE_DEFAULT,
            null
        );

        // isSetFlag($path) is !!getValue($path).
        $enabled = $this->scopeConfig->isSetFlag('loyalty/general/enabled');

        return $this->resultJsonFactory->create()->setData([
            'enabled' => $enabled,
            'rate' => (int) $rate,
            'same' => $rate === $rateLong,
        ]);
    }
}

Checking the effective value from the CLI

config:show with a full three-segment path prints only the value. Given a shorter path it prints every field underneath, one per line, prefixed by its path. This is the fastest way to answer "is my config.xml default actually being read, or is a database row shadowing it" — if config:show disagrees with your file, a core_config_data row exists.

Example · bash
$ bin/magento config:show loyalty/general/points_per_currency
10

$ bin/magento config:set loyalty/general/points_per_currency 25
Value was saved.

$ bin/magento config:show loyalty/general/points_per_currency
25

$ bin/magento config:show loyalty/general
loyalty/general/enabled - 1
loyalty/general/points_per_currency - 25

Overriding another module's default

Magento_Catalog sets catalog/frontend/grid_per_page to 12 in its own config.xml. Declaring the same node here raises the shipped default to 24 for every installation of your build, with no admin step and no database row. The merge is ordered by module load order, so your module.xml must list Magento_Catalog in its <sequence> for your value to win. Run bin/magento setup:upgrade after changing the sequence, then bin/magento cache:flush config.

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>
        <catalog>
            <frontend>
                <grid_per_page>24</grid_per_page>
            </frontend>
        </catalog>
    </default>
</config>

Discussion

  • Be the first to comment on this lesson.