This post walks through a recent lightweight extension designed to demonstrate some basic concepts and components needed for a Magento 2 extension. Code is available at https://github.com/coldgreentea/m2extension.
(Hi all! This is another guest post by Dale Sikkema on a demo that some of the internal developers put together. Enjoy! Alan)
This extension is by no means comprehensive of a real world extension. This post does not go through the installation details here — those can be found in the repository’s README. It is recommended to keep the source code open while reading this post. If you haven’t already looked into Composer, reviewing that would also be a boon!
The extension is basically “Hello World.” Accessed via <base url>/demo_extension/index/sayhello, it will produce some plain html output. Of course, you never really want to spit out raw HTML from your PHP code like this, but here we won’t focus on the view layer. In addition to “Hello World”, you’ll see a list of currently installed modules printed out. That’s just a sanity check – proof that Magento has acknowledged the extension’s existence.
Connecting to Magento
In the module’s etc directory, you will find a file called module.xml. In general, this file may contain information like the name of the module and, if it depends on other modules, the sequence in which those dependencies ought to be loaded. Here it only contains the name. This is the module’s identifier in the system.
The second part of making Magento aware of the new extension involves making sure that the system config file is updated. app/etc/config.php can be updated manually by adding the extension to the array of modules with a value of 1 indicating that it is enabled. (A tool is planned to do this for you but not yet available.) The order of modules in the array indicates the order in which they get loaded, which can matter for dependencies. This file gets updated automatically on a fresh installation. The following shows adding the new module to the end of the file.
'modules' => array ( 'Magento_Core' => 1, ... 'Magento_Wishlist' => 1, 'M2Demo_M2Extension' => 1 ),
In root of the module, there is a composer.json file. This file contains marshaling information describing where the module should be placed once composer downloads it (Magento’s composer tool uses this information to do the marshaling), dependencies on other packages, and other composer-related metadata.
In addition to this one, which is specific to the module, the Magento system uses another composer.json file that is located at the Magento root. It also plays a role in integrating the module. To download and deploy the module using the composer update command, the module’s unique composer name (ours is m2demo/module-m2-extension) must be added to the dependencies list.
When the update command is run, composer will find the module’s package (registered on packagist.org) and download it (along with whatever dependencies the module carries along with it — composer works all that out nicely). Then the autoload section should be updated for the new module’s namespace (that’s all in the README) — Magento uses composer for autoloading classes, so this will help with performance.
In the module-level composer.json, one of the dependencies is for a special tool used to deploy Magento modules from composer. This tool uses information in the extra->map section to decide where to move the module once it gets downloaded. Initially, the module will go into composer’s default download directory. Usually, that is a directory called vendor located in the same place as the composer.json which defined the dependency. The root-level composer.json defines the dependency on the new module, so the default location for downloaded packages may be <magento2-root>/vendor. This tool copies the module from there into the location specified in the module-level composer.json — in this case, app/code/M2Demo/M2Extension. It gets that location because app/code is the modules directory, and M2Demo/M2Extension is the extra bit from composer.json.
The routes.xml file defines a connection between the module and a router. Magento contains multiple routers, all of which map the URL to a particular module, and specifically, a Controller class within it. The module identified by the name attribute, listed in the module element of routes.xml, must be associated with a router. Each router has a unique identifier, so in the XML, the router element’s id attribute points out which router to the extension gets registered to. Our extension is registered to the router called standard. There are some other routers, but they are not commonly used. The route element contains two attributes – the frontName should be a readable, unique alias that will appear in the URL to signify the module being accessed, and the id is a unique identifier for the connection between router and module (used internally for sorting purposes).
The standard router is given the URL and breaks it down into routing parameters separated by slashes. In <base url>/demo_extension/index/sayhello, the three routing parameters are “demo_extension”, “index”, and “say hello”. In that order, they refer to the module’s frontname, the action path, and the action name. Here’s how that works out in the file system – the frontname points to the directory of the module, and the other two parameters point to: <module-directory>/Controller/<action path>/<action name>.php.
The one class in this extension, SayHello, is a controller which defines its dependencies in its constructor. Magento’s architecture is based on this idea of accessing dependencies through the constructor — Dependency Injection. We don’t have to manually instantiate or initialize those dependencies as there is an object management system operating “behind the scenes” (as far as our extension goes) which handles them. The router, after analyzing the URL, finds this controller class and uses ObjectManager to instantiate it. ObjectManager scans for dependencies in the constructor and instantiates them recursively.
A few more remarks about Dependency Injection while we’re at it. One of the dependencies in SayHello is not actually a class but an interface. That means there is configuration information which maps any given interface to the preferred concrete class that will be injected in its place by ObjectManager. These dependency injection configurations are specified in files called di.xml. There is much to say about these files, but not for this post.
Even though our extension lacks its own configuration files for dependency injection, the controller relies on a mapping that is defined in the root level DI config: app/etc/di.xml. There, Magento\Framework\Module\ModuleListInterface gets associated with the concrete class Magento\Framework\Module\ModuleList.
As an aside, Dependency Injection also enables Magento’s powerful framework for plugins. This extension does not use this feature, but while I’ve got you thinking about object management, think about this: ObjectManager can instantiate a totally different class than the one specified by the constructor (the magic of inheritance!), a wrapper class which enhances or intercepts the original class’s behavior. Classes that have plugins get special wrappers automatically generated for them which enable the plugin behavior.
Take a look at the namespace for the SayHello class. The namespace must follow PSR-4 conventions. This means that the first part of the namespace, the prefix, must correspond to a certain base path in the filesystem. This correspondence is defined in the root composer.json, where the “M2Demo” prefix maps to “app/code/M2Demo”. All of the parts of the namespace after the prefix must correspond to directories under the base path, and the class name itself corresponds to the name of the file containing the class.
When a new class, like our controller, needs to be instantiated, the autoloader uses its fully qualified name to locate and include the necessary file. Many autoloader implementations can be registered to the PHP process – Magento primarily uses Composer’s autoloading technology. A word for the wise – if you ever see something like “such and such class cannot be found,” make sure the namespace lines up with the directory structure just right (it’s bitten me a few times).
As you can see, there is a lot going on beneath the hood here. While this demo is awfully simple to be incorporating an object manager, a sophisticated system of routers and controllers, and gads more bells and whistle’s than I’ve described, these components enable an incredibly flexible range of development options for you to try out. So please, tinker! See what magery you can pull off with the Magento 2 framework.
Excellent post, Dale! Please add an introduction like this to the Extension Developers section of http://devdocs.magento.com/.
The documentation team pretty much could copy and paste the text as it covers the most important bits to get going. There’s always time to refine and expand on it later but most developers are checking the dev docs now and may be missing such a tutorial.
It would be great if this extension could include also some functional, integration and/or unit tests.
This is nice tutorial for learn magneto extension development process