Area-Specific Configuration

Each area gets its own merged DI configuration, so etc/adminhtml/di.xml can replace a class the storefront never sees.

Syntaxapp/code/Vendor/Module/etc/<area>/di.xml

An area is the runtime context a request belongs to: frontend for the storefront, adminhtml for the admin, plus webapi_rest, webapi_soap, graphql, crontab, and the global context that belongs to no request. Each area has its own dependency injection configuration.

Which file wins

Files are merged in order: app/etc/di.xml first, then every enabled module's etc/di.xml in module load order, then the area files at etc/<area>/di.xml. Later wins, so an area file always beats a global one. Core depends on this. app/etc/di.xml maps Magento\Framework\UrlInterface to Magento\Framework\Url, while app/code/Magento/Backend/etc/adminhtml/di.xml maps the same interface to Magento\Backend\Model\UrlInterface, which resolves in turn to Magento\Backend\Model\Url. The same injected parameter therefore builds storefront URLs in one area and admin URLs in the other.

Where the merged result lives

The result is cached per area under the config cache type, and setup:di:compile writes it to generated/metadata/<area>.php. After editing any di.xml run bin/magento cache:clean config; in production run bin/magento setup:di:compile.

What trips people up

Areas do not inherit from one another. A preference in etc/frontend/di.xml has no effect on a REST call, a GraphQL query, a cron job or a console command, and console commands run in the global area. Confirm what you got with bin/magento dev:di:info "Magento\Framework\UrlInterface" adminhtml.

Example

Example · xml
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/adminhtml/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <!-- Admin requests get the internal labels, with SKUs and cost. -->
    <preference for="Vendor\Module\Api\LabelFormatterInterface"
                type="Vendor\Module\Model\LabelFormatter\Internal"/>

    <type name="Vendor\Module\Model\Export\Writer">
        <arguments>
            <argument name="includeDisabled" xsi:type="boolean">true</argument>
        </arguments>
    </type>
</config>

When to use it

  • Use a different implementation of your own interface in the admin than on the storefront.
  • Work out why a preference that works on the storefront is ignored by a REST endpoint.
  • Decide whether a new di.xml entry belongs in etc/di.xml or in etc/adminhtml/di.xml.
  • Explain to a reviewer which file is winning when two modules configure the same class.

More examples

The global file this area file overrides

Read the two files as a pair. The storefront, REST, GraphQL and cron all get the public formatter and skip disabled products; only admin requests get the internal formatter and include them. There is no duplication to keep in step, because the area file states just the two differences. Both files are applied by bin/magento cache:clean config, and by setup:di:compile in production.

Example · xml
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <!-- Everywhere except the admin: customer-facing labels. -->
    <preference for="Vendor\Module\Api\LabelFormatterInterface"
                type="Vendor\Module\Model\LabelFormatter\Storefront"/>

    <type name="Vendor\Module\Model\Export\Writer">
        <arguments>
            <argument name="includeDisabled" xsi:type="boolean">false</argument>
        </arguments>
    </type>
</config>

Confirming the merge rather than assuming it

The area is a second positional argument and defaults to global, which is why a frontend or adminhtml override seems to be missing until you ask for it. The printed preference is followed all the way down the chain, so adminhtml reports Magento\Backend\Model\Url even though the area file names Magento\Backend\Model\UrlInterface. Only registered area codes are accepted: adminhtml is an area, admin is a route front name.

Example · shell
bin/magento dev:di:info "Magento\Framework\UrlInterface"
#
# DI configuration for the class Magento\Framework\UrlInterface in the GLOBAL area
#
# Preference: Magento\Framework\Url

bin/magento dev:di:info "Magento\Framework\UrlInterface" adminhtml
#
# DI configuration for the class Magento\Framework\UrlInterface in the ADMINHTML area
#
# Preference: Magento\Backend\Model\Url

bin/magento dev:di:info "Magento\Framework\UrlInterface" admin
# The "admin" area code does not exist

How an area comes to exist at all

The list of areas is itself an array argument that modules add to: Magento_Store registers frontend, Magento_Backend registers adminhtml, Magento_Webapi registers webapi_rest and webapi_soap, Magento_Cron registers crontab. That is why generated/metadata holds one file per area, and why dev:di:info rejects a code no module has registered. Reading this is usually enough to explain where an area name came from.

Example · xml
<!-- app/code/Magento/Backend/etc/di.xml -->
<type name="Magento\Framework\App\AreaList">
    <arguments>
        <argument name="areas" xsi:type="array">
            <item name="adminhtml" xsi:type="array">
                <item name="frontNameResolver" xsi:type="string">Magento\Backend\App\Area\FrontNameResolver</item>
                <item name="router" xsi:type="string">admin</item>
            </item>
        </argument>
    </arguments>
</type>

<!-- app/code/Magento/GraphQl/etc/di.xml -->
<type name="Magento\Framework\App\AreaList">
    <arguments>
        <argument name="areas" xsi:type="array">
            <item name="graphql" xsi:type="array">
                <item name="frontName" xsi:type="string">graphql</item>
            </item>
        </argument>
    </arguments>
</type>

Discussion

  • Be the first to comment on this lesson.