The Architecture in One Pass

Modules, dependency injection, plugins, layout XML, EAV and indexers named once, so the words are familiar before each is taught.

Syntaxpub/index.php -> front controller -> router -> controller -> result -> layout -> blocks -> templates

Magento is assembled rather than written. Classes are swapped by configuration, pages are merged from XML, and a product's fields are rows instead of columns. Here are the words. Each has its own stage later.

WordWhat it means
ModuleA directory of PHP and XML, named Vendor_Module, that registers itself and can be enabled or disabled.
AreaWhich part of the application is handling the request: frontend, adminhtml, webapi_rest, crontab. Configuration is loaded per area.
Object managerThe container that builds objects. You do not call it. You declare constructor parameters and it supplies them.
PreferenceA line of di.xml that says: when anything asks for this interface, give it that class.
Virtual typeA pre-configured variant of an existing class, given a new name in di.xml with no PHP file.
PluginCode that runs before, after or around a public method of another class. Magento applies it by generating an interceptor subclass.
Layout XMLA page is a tree merged from many modules' XML, chosen by a handle such as catalog_product_view.
EAVEntity-attribute-value: product fields are rows in attribute tables, grouped by attribute set.
IndexerA job that flattens that data into fast read tables; mview is the change log that lets it update only what moved.

What trips people up

Two layers sit between your file and the running code: generated classes in generated/ and cached configuration. After editing XML run bin/magento cache:flush. After changing a constructor in production mode run bin/magento setup:di:compile.

Example

Example · php
<?php
// app/code/Vendor/Storefront/Service/StoreGreeting.php

declare(strict_types=1);

namespace Vendor\Storefront\Service;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;

class StoreGreeting
{
    private ScopeConfigInterface $scopeConfig;

    public function __construct(ScopeConfigInterface $scopeConfig)
    {
        $this->scopeConfig = $scopeConfig;
    }

    public function forStore(?int $storeId = null): string
    {
        $name = $this->scopeConfig->getValue(
            'general/store_information/name',
            ScopeInterface::SCOPE_STORE,
            $storeId
        );

        return sprintf('Welcome to %s', $name ?? 'our store');
    }
}

// Output when Store Information > Store Name is "Soundscode":
// Welcome to Soundscode

When to use it

  • Working out which layer to change when a requirement does not fit an existing extension point.
  • Reading a colleague's module and recognising di.xml, layout XML and plugin declarations on sight.
  • Explaining to a non-Magento PHP developer why a change needs a command to take effect.

More examples

A module names itself and its load order

The one file that makes a directory a module in the application's eyes, alongside registration.php. <sequence> does not create a dependency; it only says "load after these", which matters when your XML overrides theirs. Changing this file takes effect on bin/magento setup:upgrade.

Example · xml
<!-- app/code/Vendor/Storefront/etc/module.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_Storefront">
        <sequence>
            <module name="Magento_Catalog"/>
            <module name="Magento_Store"/>
        </sequence>
    </module>
</config>

The same class in shorthand

Constructor property promotion, available since PHP 8.0, declares the property and assigns it in one place. Dependency injection behaves identically because the container reads the constructor signature either way. Magento's own code still uses the long form, so you will meet both and should recognise both.

Example · php
<?php
// app/code/Vendor/Storefront/Service/StoreGreeting.php

declare(strict_types=1);

namespace Vendor\Storefront\Service;

use Magento\Framework\App\Config\ScopeConfigInterface;

class StoreGreeting
{
    public function __construct(
        private ScopeConfigInterface $scopeConfig
    ) {
    }

    // ... methods unchanged
}

// Longhand, which this desugars to exactly:
//
//     private ScopeConfigInterface $scopeConfig;
//
//     public function __construct(ScopeConfigInterface $scopeConfig)
//     {
//         $this->scopeConfig = $scopeConfig;
//     }

Interception is a generated subclass

Useful when a plugin you declared appears to do nothing. In developer mode these classes are written on the first request that needs them, so deleting the tree is enough; in production mode nothing is generated at runtime and setup:di:compile must run.

Example · bash
# a plugin never edits the target class; it produces this
ls generated/code/Magento/Catalog/Model/Product/
# Interceptor.php

# the tree is disposable - delete it when it looks stale
rm -rf generated/code generated/metadata
bin/magento setup:di:compile
# Compilation was started.
# ...
# Generated code and dependency injection configuration successfully.

Discussion

  • Be the first to comment on this lesson.