Chat on WhatsApp
Back to /claude
Claude code cluster

Magento 2 Module Anatomy Explained, File by File

Open up a Magento 2 module and look inside. This file-by-file map walks every directory you will see in a real production module — from registration.php through GraphQL schema, MFTF tests, and CLAUDE.md — so nothing is a black box.

ARCHITECTURE

Anatomy of a Magento module

Click any file to learn what it does, when you need it, and which Magento subsystem it talks to.

PATH
WHEN YOU NEED IT

WHAT IT DOES

WHY IT MATTERS

Try this Claude prompt

Pick any node from the tree to see when you need it, what it does, and a Claude Code prompt that scaffolds it for you.

More in the cluster

Keep going

FAQ

Common questions

What is the difference between registration.php and etc/module.xml?

registration.php tells Composer's autoloader where the module lives on disk — it runs at bootstrap and registers the namespace + path with ComponentRegistrar. Without it, Magento literally cannot find your code. etc/module.xml tells Magento's module manager the module exists, what its setup_version is, and its load-order dependencies via <sequence>. You need both: registration.php for the autoloader, module.xml for module:enable. A common bug is a working registration.php with a missing module.xml — the class loads, but DI etc/di.xml is silently ignored because the module is not enabled.

Why does my plugin in etc/di.xml not fire on the storefront?

You almost certainly put it in etc/frontend/di.xml when you needed it in etc/di.xml — or vice-versa. Magento has four DI scopes: etc/di.xml (global), etc/frontend/di.xml (storefront only), etc/adminhtml/di.xml (admin only), etc/webapi_rest/di.xml + webapi_soap/di.xml (REST/SOAP). Plugins on storefront-only services (e.g. CustomerSession) live under etc/frontend/. Plugins on universal services (e.g. OrderRepository) live in global etc/di.xml. Run bin/magento setup:di:compile after every move — the compiled interceptors are area-scoped and won't pick up the change otherwise.

Should I use db_schema.xml or InstallData / UpgradeData scripts?

db_schema.xml — always, for table structure. The legacy InstallSchema / UpgradeSchema classes are deprecated since 2.3 and removed-in-spirit since 2.4. Declarative schema lets you diff via setup:db-declaration:generate-whitelist, supports rollback, and survives setup:upgrade --safe-mode. For data seeds (config rows, EAV attributes, sample products), use Setup/Patch/Data/<PatchName>.php implementing DataPatchInterface. Patches run once, are tracked in patch_list, and are idempotent if you write them right. Don't mix the two — declarative schema for structure, data patches for content.

How do ACL resources in etc/acl.xml actually gate admin access?

An ACL resource is just a string ID like Vendor_Module::manage_widgets. You declare it in etc/acl.xml under the Magento_Backend::admin tree, then reference it in three places: (1) etc/adminhtml/menu.xml — the resource attribute hides the menu item from users without the role; (2) the controller's _isAllowed() method — gates the page itself; (3) etc/adminhtml/system.xml — gates config sections. ACL is permission-only — it doesn't authenticate, it doesn't audit, it just answers yes/no. Always implement _isAllowed(); menu hiding is cosmetic, anyone with the URL can hit the controller without it.

Do I need both etc/webapi.xml and etc/schema.graphqls for one feature?

Only if you want both REST and GraphQL clients. The good pattern: build a Service Contract (Api/ + Api/Data/ + Repository) once, then expose it through whichever transport(s) you need. etc/webapi.xml maps an HTTP route + method to a service interface method — pure declarative. etc/schema.graphqls defines a GraphQL type and field, and a Resolver class translates the GraphQL args into a service-contract call. Both transports re-use the same Repository, so business logic lives in one place and you avoid the classic Magento 2 trap of REST and GraphQL drifting out of sync.