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.
pub/index.php -> front controller -> router -> controller -> result -> layout -> blocks -> templatesMagento 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.
| Word | What it means |
|---|---|
| Module | A directory of PHP and XML, named Vendor_Module, that registers itself and can be enabled or disabled. |
| Area | Which part of the application is handling the request: frontend, adminhtml, webapi_rest, crontab. Configuration is loaded per area. |
| Object manager | The container that builds objects. You do not call it. You declare constructor parameters and it supplies them. |
| Preference | A line of di.xml that says: when anything asks for this interface, give it that class. |
| Virtual type | A pre-configured variant of an existing class, given a new name in di.xml with no PHP file. |
| Plugin | Code that runs before, after or around a public method of another class. Magento applies it by generating an interceptor subclass. |
| Layout XML | A page is a tree merged from many modules' XML, chosen by a handle such as catalog_product_view. |
| EAV | Entity-attribute-value: product fields are rows in attribute tables, grouped by attribute set. |
| Indexer | A 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
<?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 SoundscodeWhen 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.
<!-- 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.
<?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.
# 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