- Magento 2: Composer, Marketplace, and Satis
- Magento 2: Composer Plugins
- Magento 2: Composer and Components
- Magento, Composer, and Autoload Patterns
One question I keep getting from new Magento 2 developers, (and we’re all new Magento 2 developers) is, “How should I organize my project files?”. Since Magento has heavily restructured code organization around Composer, it’s not always clear how files should be organized during development, organized for distribution, and how (if at all) to move between these two modes.
While this article won’t definitively answer those questions, we will dive into some of the formalization behind Magento 2 components, as well as how Magento 2’s Composer integration works. With this information in hand, you should be able to come up with a project structure that works for you and your team.
Magento 2 Components
Magento 1 had an informal, inconsistent idea of components. The best way to think about Magento components is
A group of source files, in various formats, with a single purpose in the system
If that’s a little vague, see our previous comments about informal and inconsistent. Speaking more concretely, the four component types in Magento are
- Modules
- Themes
- Language Packs
- Code Libraries
In Magento 1, modules were pretty well defined and self contained
A folder of files with an
etc/config.xml
, with developers making Magento aware of the module via anapp/etc/modules/Package_Namespace.xml
file
Themes were a little less defined and a little less self contained
A collection of files under
app/design/[area]/[package]/[name]
, with developers making Magento aware of the theme via a setting incore_config_data
. Unless it’s the admin theme in which case you need a module. Also, modules are responsible for adding the layout XML files to themes. Also, while we’re here, modules aren’t all that self contained either because if they want to usephtml
templates then the templates need to be in a theme folder
Things start to get really vague with language packs
A collection of key/value
csv
files located inapp/locale/[language]/Packagename_Modulename.csv
. Also we’re just going to drop email templates in here because reasons
And Magento 1 barely had the concept of a generic code library.
Um, yeah, maybe just drop them in
lib
and add that path to PHP’s autoloader? And look in the code pool folders too? And maybe just add a top leveljs
folder for javascript libraries? Unless they go inskin
?
Oh right! Skins! Magento 1 also had a (now dropped) concept of skins. Skins were best defined as
Any CSS or javascript file that doesn’t belong in a theme.
Where CSS or javascript file that doesn’t belong in a theme was defined as
Any CSS or javascript file that doesn’t belong in a skin
While, in practice, norms developed over time and development wasn’t as chaotic as I’m describing, Magento 1’s lack of formalization around components did make the system harder to work with, particularly if you were trying to redistribute Magento 1 code for reuse.
Magento 2 formalizes the idea of components, and this formalization means the core Magento system, and other external systems (i.e. Composer) can deal with these components in a sane and reasonable way.
Magento 2 Components
In Magento 2, a component is
A group of files, under a top level directory (with sub-folders allowed), with a
registration.php
file defining the type of component.
That’s it. There’s nothing about how the components work, interact with the system, or interact with other components. Those things aren’t the concern of the component system.
As of this writing, there are four component types in Magento 2
- Modules
- Themes
- Libraries
- Language Packs
Let’s take a look at this in action. Consider the Magento_AdminNotification
module. This is a collection of files, under a top level directory (AdminNotification
), with a registration.php
file. If we take a look at registration.php
#File: app/code/Magento/AdminNotification/registration.php
<?php
/**
* Copyright © 2016 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Magento_AdminNotification',
__DIR__
);
We can see this file registers a component via the static \Magento\Framework\Component\ComponentRegistrar::register
method. This component is a module (\Magento\Framework\Component\ComponentRegistrar::MODULE
), its identifier is Magento_AdminNotification
, and you can find its files in the __DIR__
folder, (i.e. the same directory registration.php
is in via PHP’s magic __DIR__
constant).
Next, consider the Luma theme. Again, a collection of files, under a top level directory (luma
), with a registration.php
file.
#File: app/design/frontend/Magento/luma/registration.php
<?php
/**
* Copyright © 2016 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::THEME,
'frontend/Magento/luma',
__DIR__
);
Here the component type is a theme (\Magento\Framework\Component\ComponentRegistrar::THEME
), and its name is frontend/Magento/luma
.
Even though the Magento GitHub project has these files in familiar locations, (app/code
, app/design
, etc.), thanks to Magento 2’s new component system, these directories can be located anywhere, so long as as the module, theme, library, or language pack correctly defines its registration.php
file.
How Magento Loads Components
At this point, the systems minded among you are probably wondering how Magento 2 loads and identifies components. It’s one thing to say so long as the module, theme, library, or language pack correctly defines its registration.php
file, but the system still needs to load these files, and that means there are rules.
In order to get Magento to recognize your module, theme, code library, or language pack (i.e. your component), you need Magento to read your component’s registration.php
file. There are two ways to get Magento to read your registration.php
file
- Place your component in one of several predefined folders
- Distribute your module via Composer, and use Composer’s autoloader features
Of the two methods, the second is the preferred and recommended way of distributing Magento 2 modules. For development, the first offers a convenient way to get started on a component, or checkout/clone a version control repository to a specific location. The first also offers a non-composer way for extension developers to distribute their components.
Predefined Folders
At the time of this writing, Magento 2 will scan the following folders/files for components (the patterns below are for the glob function).
app/code/*/*/cli_commands.php
app/code/*/*/registration.php
app/design/*/*/*/registration.php
app/i18n/*/*/registration.php
lib/internal/*/*/registration.php
lib/internal/*/*/*/registration.php
This is what allows you to place modules in app/code/Packagename/Modulename
, or themes in app/design/[area]/[package]/[name]
, etc. Magento will explicitly look for registration.php
files to load at these locations. Also of interest are the app/code/*/*/cli_commands.php
files — this appears to be a way for a module to register command line classes without using di.xml
.
It’s not clear if these folders were added as a stop-gap measure while Magento 2 gets everyone moved over to Composer distribution, or if they’ll stick around for the long term. If you’re curious, Magento does this registration check in the following file
#File: app/etc/NonComposerComponentRegistration.php
<?php
/**
* Copyright © 2015 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
$pathList[] = dirname(__DIR__) . '/code/*/*/cli_commands.php';
$pathList[] = dirname(__DIR__) . '/code/*/*/registration.php';
$pathList[] = dirname(__DIR__) . '/design/*/*/*/registration.php';
$pathList[] = dirname(__DIR__) . '/i18n/*/*/registration.php';
$pathList[] = dirname(dirname(__DIR__)) . '/lib/internal/*/*/registration.php';
$pathList[] = dirname(dirname(__DIR__)) . '/lib/internal/*/*/*/registration.php';
foreach ($pathList as $path) {
// Sorting is disabled intentionally for performance improvement
$files = glob($path, GLOB_NOSORT);
if ($files === false) {
throw new \RuntimeException('glob() returned error while searching in \'' . $path . '\'');
}
foreach ($files as $file) {
include $file;
}
}
If you’re researching how a future version of Magento 2 handles scanning for components, this would be a good place to start.
Composer Distribution
The other way to have Magento notice your component is to distribute your component via Composer. We’re going to assume you have a basic familiarity with Composer, but for the purposes of this article, all you really need to know is
Composer allows you to ask for a package of PHP files, and have that package downloaded to the
vendor/
folder
If you’re interested in learning more about Composer, the previous articles in this series, my Laravel, Composer, and the State of Autoloading, and the Composer manual are a good place to start.
So, assuming you have your Magento component in GitHub (or a different source repository Composer can point at), and your component has a registration.php
file, the only question left is How do we get Magento to look at our registration.php
file.
Rather than have Magento scan all of vendor/
for registration.php
files, (an approach that could quickly get “O^N
out of hand” as the number packages grows), Magento uses Composer’s file
autoloader feature to load each individual component’s registration.php
file.
If that didn’t make sense, an example should clear things up. Assuming you’ve installed Magento via the Composer meta-package, or installed it via the archive available via magento.com (which is based on the meta-package), take a look at the catalog module’s composer.json
file.
#File: vendor/magento/module-catalog/composer.json
{
"name": "magento/module-catalog",
//...
"autoload": {
"files": [
"registration.php"
],
"psr-4": {
"Magento\\Catalog\\": ""
}
}
//...
}
The autoload section is where you configure the PHP class autoloader for a Composer package. This is covered in great detail in my Laravel, Composer, and the State of Autoloading series. The section we’re interested in today is here
#File: vendor/magento/module-catalog/composer.json
"files": [
"registration.php"
],
The files
autoloader section was originally intended as a stop gap measure for older PHP packages that had not moved to a PSR-0
(and later, PSR-4
) autoloader system. Composer’s autoloader (not during install
or update
, but when your application is running) will automatically include any files listed in files
(with the specific package as the base directory), and package developers can do whatever they need to do to setup their pre-PSR autoloaders.
Over the years, many frameworks have taken the simplicity and flexibility of the files
autoloader and turned it to different purposes. Magento 2 is no exception. The above autoload configuration ensures Composer will always load the file at
#File: vendor/magento/module-catalog/registration.php
<?php
/**
* Copyright © 2015 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Magento_Catalog',
__DIR__
);
This, as we’ve already learned, will register the component. The same holds true for third party components (i.e. yours!) — make sure you’ve created a registration.php
file with the correct registration code for your component type, and then include an identical files
autoloader.
Here’s an example of each component type from Magento’s core.
Module
#File: vendor/magento/module-weee/registration.php
<?php
/**
* Copyright © 2015 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Magento_Weee',
__DIR__
);
Theme
#File: vendor/magento/theme-frontend-luma/registration.php
<?php
/**
* Copyright © 2015 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::THEME,
'frontend/Magento/luma',
__DIR__
);
Library
#File: vendor/magento/framework/registration.php
<?php
/**
* Copyright © 2015 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::LIBRARY,
'magento/framework',
__DIR__
);
Language Pack
#File: vendor/magento/language-de_de/registration.php
<?php
/**
* Copyright © 2015 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::LANGUAGE,
'magento_de_de',
__DIR__
);
Invalid Assumptions
There’s one last important thing to take away from this, even if you’re not responsible for packaging your company’s Magento work. It’s no longer safe to make assumptions about where a folder is located in located in the Magento hierarchy. If you’re trying to find a specific file in Magento, it’s more important than ever to learn your way around the Magento\Framework\Module\Dir
helper class.
Daily Work
So, now that we have a better understanding of what a component is, and how Magento loads components into the system, that still leaves us with our original question. Where should the code for our in progress Magento projects go? How should we store our projects in source control?
Unfortunately — there’s no clear answer, and a lot will depend on the sort of project you’re working on. Are you an extension developer? A theme developer? A system integrator/store builder or someone integrating with a Magento system? Do you want your working source repository to be the same repository Composer reads from? What tooling is your team is familiar with? While there certainly are approaches that are “better” for each scenario, from a programmer’s point of view Magento 2’s still too new to know for sure.
For what its worth, I’ve been creating symlinks to my source repositories so that NonComposerComponentRegistration.php
finds my components, using a build process to create the final Composer accessible repository, and temporarily patching over any issues Magento has with symlinks.
Part of being a Magento 2 developer will be figuring this out for your own team, even if you’re just a team of one.