What a Module Is
A module is a Vendor/Module folder under app/code, made real by registration.php and etc/module.xml.
app/code/<Vendor>/<ModuleName>/registration.php + app/code/<Vendor>/<ModuleName>/etc/module.xmlMagento is not a framework you bolt features onto from outside. Everything is a module: a directory of PHP and XML that adds one feature and can be switched off. The checkout is a module. The admin notifications are a module. Your code is a module too.
A module has a two-part name, a vendor and a module name joined by an underscore, as in Acme_Faq. The directory mirrors the name.
app/code/Acme/Faq/ ├── registration.php ├── etc/ │ └── module.xml └── composer.json (optional while the module lives in app/code)
The two files that make it exist
registration.php tells Magento the module's name and its absolute path. etc/module.xml declares the module to the configuration layer and can name modules that must load before it. The two halves fail differently when one is missing. A directory without registration.php is never registered, so it is simply absent and nothing is reported. A module that registers itself but ships no etc/module.xml is worse: Magento\Framework\Module\ModuleList\Loader walks every registered module path and reads <path>/etc/module.xml unconditionally, so the missing file throws a FileSystemException on every request.
Why exactly two levels deep
At boot Magento reads the glob patterns in app/etc/registration_globlist.php. For modules the pattern is app/code/*/*/registration.php: one vendor folder, one module folder. A file at app/code/Faq/registration.php, or one nested a level deeper, is never found.
What trips people up
Creating the files does not switch the module on. Run bin/magento setup:upgrade, which scans app/code, records anything new in app/etc/config.php as enabled and applies its schema. Then run bin/magento cache:flush, because the merged configuration is cached. In production mode also run bin/magento setup:di:compile.
Example
<?php
// app/code/Acme/Faq/registration.php
declare(strict_types=1);
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Acme_Faq', __DIR__);
When to use it
- Starting a new feature and getting Magento to see a fresh app/code/Acme/Faq directory.
- Working out why a module copied onto a server does not appear in bin/magento module:status.
- Deciding whether new code belongs in app/code or in a Composer package under vendor.
- Reading an unfamiliar third-party extension by starting from its registration.php and module.xml.
More examples
etc/module.xml, the second required file
The smallest valid module.xml. The name must match the name passed to ComponentRegistrar exactly. This file is what makes Magento treat the directory as a configuration source, so its etc/ files are merged into the application config. After adding it, run bin/magento setup:upgrade and bin/magento cache:flush.
<!-- app/code/Acme/Faq/etc/module.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Acme_Faq"/>
</config>
The same mechanism registers a theme
Modules are one kind of component. Themes, language packages and libraries register through the same static call with a different type constant and a different name format. Note the path: themes are globbed as app/design/*/*/*/registration.php, three levels, because the area name is part of the path. Reach for this when you need to tell whether an unfamiliar directory is a module or a theme: read its registration.php.
<?php
// app/design/frontend/Acme/aurora/registration.php
declare(strict_types=1);
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::THEME, 'frontend/Acme/aurora', __DIR__);
A module installed under vendor/ instead
Under vendor/, the glob never runs. Composer's generated autoloader includes every path in autoload.files, which is how registration.php executes, and psr-4 resolves the classes. The module is identical in every other way, which is why a third-party extension and your own app/code module look the same from inside. Use this form once the module has to be installed into more than one project.
{
"name": "acme/module-faq",
"type": "magento2-module",
"autoload": {
"files": [
"registration.php"
],
"psr-4": {
"Acme\\Faq\\": ""
}
}
}
Discussion