- Introduction to Magento 2 — No More MVC
- Magento 2: Serving Frontend Files
- Magento 2: Adding Frontend Files to your Module
- Magento 2: Code Generation with Pestle
- Magento 2: Adding Frontend Assets via Layout XML
- Magento 2 and RequireJS
- Magento 2 and the Less CSS Preprocessor
- Magento 2: CRUD Models for Database Access
- Magento 2: Understanding Object Repositories
- Magento 2: Understanding Access Control List Rules
- Magento 2: Admin Menu Items
- Magento 2: Advanced Routing
- Magento 2: Admin MVC/MVVM Endpoints
In the first article of this series, we described how to create a simple MVC/MVVM endpoint in Magento 2. Implicit, but unstated, was that we were setting up an endpoint for Magento’s frontend cart application. While the backend admin application uses the same MVC/MVVM/View system as the frontend, there’s additional features based on backend security and user interface conventions in Magento 2.
Today we’re going to start exploring the Menu Item system. Our end goal is to add a link to Magento’s left side admin application which, as you’ll learn, is the first step towards adding a backend page to Magento 2.
Adding a Menu to the Left Side
Menu Items are the links along the left side of the Magento admin console application. For example — if you navigate to
Content -> Elements -> Pages
the Pages hyperlink is a Menu Item. If you right click on this hyperlink and select Copy Link Address, you’ll see something like this
http://magento.example.xom/admin/cms/page/index/key/ed2ddfe814ba40acb42b6fd4e95be717d32528860c3960d5e178b50e3691e0b0/
There’s a few things we’ll want to make note of with regards to the structure of an admin URL. First, admin URLs have a four segment structure.
admin/cms/page/index
The first segment, admin
, is the area. All URLs in the Magento admin start with this additional URL segment. The next three segments are the URL front name, controller name, and action name.
Front Name: cms
Controller Name: page
Action Name: index
Same as a standard frontend controller, Magento 2 combines all three of these segments to create a single controller class name. Magento 2 is a “One URL, One Controller” system. If you’re not sure what that means, don’t worry, future examples below will clear that up.
Finally, all Menu Items have an additional URL parameter named key
, with a corresponding value
key/ed2ddfe814ba40acb42b6fd4e95be717d32528860c3960d5e178b50e3691e0b0/
This special code is required for all URLs, and is here to help prevent cross site script attacks. If you fail to include this key with your URL, Magento will reject the request as invalid.
This URL key is why we need to create a Menu Item in the first place — without Magento generating a URL key for us, there’s no simple safe way to access a standard admin controller.
Creating a Menu Item
We’re going to dive right in and create a new Menu Item. First, we’ll need to create a Magento module to hold our Menu Item. You can create the base module files using the pestle command line framework’s generate_module
command.
$ pestle.phar generate_module Pulsestorm MenuTutorial 0.0.1
and then enable the module in Magento by running the following two commands
$ php bin/magento module:enable Pulsestorm_MenuTutorial
$ php bin/magento setup:upgrade
If you’re interested in creating a module by hand, or curious what the above pestle command is actually doing, take a look at our Introduction to Magento 2 — No More MVC article.
Next, we’ll want to add a menu.xml
file to our module. Create the following file with the following contents.
<!-- File: app/code/Pulsestorm/MenuTutorial/etc/adminhtml/menu.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
<menu>
<add id="Pulsestorm_MenuTutorial::top_level_example"
title="Top Level Example"
module="Pulsestorm_MenuTutorial"
sortOrder="9999"
resource="Magento_Backend::content"
/>
</menu>
</config>
With the above in place, clear your Magento cache and load a backend page. You should see a new, top level Menu Item at the bottom of the admin navigation
Congratulations! You’ve just created your first Menu Item.
At its simplest, a menu.xml
file is a collection of <add/>
nodes. Each of these nodes adds a Menu Item to Magento’s backend.
The id attribute defines a unique identifier for this node. By convention, (but not required) this should the the name of the module (Pulsestorm_MenuTutorial
), followed by two colons (::
), followed by lowercase text that describes what the module does (top_level_example
).
The title attribute (Top Level Example
) controls the text an end users sees for the Menu Item.
The module attribute should match the current module — this may seem redundant with the id
attribute, but remember, it’s only convention that forces you to use the module name in a Menu Item’s id.
The sortOrder attribute controls how this Menu Item is sorted with the other Menu Items in the system. The 9999
value ensures our module shows up under all the stock Menu Items.
Finally, the resource attribute defines the ACL rule a user must have in order to access this Menu Item. Normally, you’d define your own ACL rule in the same module, and use it here. For simplicity’s sake, we’re using a predefined ACL rule (Magento_Backend::content
) from a standard Magento module. If the logged in user does not have access to the configured ACL rule, the Menu Item will not render for them.
For Magento 1 developers, its worth noting that the ACL rule requirements for a module have been somewhat simplified. All we need to do in Magento 2 is specify an id — there’s no confusing matching of ACL hierarchies with Menu Item hierarchies. The tradeoff, of course, is it’s no longer possible to infer a Menu Item’s ACL rule based on its hierarchy.
Menu Items Hyperlinks
You may have noticed our top level Menu Item doesn’t actually link to anything. While its possible for a top level Menu Item to be a hyperlink, by convention the left side navigation items are used for organization. i.e., they contain child links. Let’s try adding a new Menu Item that actually has a hyperlink. Change your menu.xml
so it matches the following
<!-- File: app/code/Pulsestorm/MenuTutorial/etc/adminhtml/menu.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
<menu>
<add id="Pulsestorm_MenuTutorial::top_level_example"
title="Top Level Example"
module="Pulsestorm_MenuTutorial"
sortOrder="9999"
resource="Magento_Backend::content"
/>
<!-- START: new node -->
<add id="Pulsestorm_MenuTutorial::second_level_example"
title="Second Level Example"
module="Pulsestorm_MenuTutorial"
sortOrder="9999"
resource="Magento_Backend::content"
parent="Pulsestorm_MenuTutorial::top_level_example"
action="cms/page/index"
/>
<!-- END: new node -->
</menu>
</config>
Here we’ve add a new <add/>
node to our menu.xml
file. We’ve given this new Menu Item a new, unique id
attribute and a new title
. Normally, this Menu Item would have a different resource
from its parent (giving systems owners fine grained control over which modules show up) but to keep things simple we’re using the same rule as our parent item (Magento_Backend::content
).
The important parts are our parent and action attributes. The parent
attribute should be a Menu Item ID that already exists, and will tell Magento this new Menu Item (Pulsestorm_MenuTutorial::second_level_example
) is a child of the parent. Magento 1 developers will want to take note — these add
nodes all exist at the same XML tree level — the parent/child relationships between them are dictated by these parent
attributes.
The action
attribute is the three segment, frontName/controllerName/actionName
Magento 2 URL identifier. We’ve borrowed an action
from the CMS module for this tutorial. Clear your cache, reload the page, and your Top Level Example Menu Item should now trigger a fly-out menu that contains your single Second Level Example link.
If you click on this link, your browser will being you to the specified action
.
If you’ve used the Magento admin, you’ve probably notices most core modules have Menu Items organized under sub-menus. You can do that with your own modules! Add the following third node to your menu.xml
file
<!-- File: app/code/Pulsestorm/MenuTutorial/etc/adminhtml/menu.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
<menu>
<!-- ... -->
<add id="Pulsestorm_MenuTutorial::third_level_example"
title="Third Level Example"
module="Pulsestorm_MenuTutorial"
sortOrder="9999"
resource="Magento_Backend::content"
parent="Pulsestorm_MenuTutorial::second_level_example"
action="cms/page/index"
/>
</menu>
</config>
Here we’ve created a third level node, and set its parent to our second level node. If you clear your cache and reload the page with the above menu in place, your fly-out menu should look like this
Notice that our second level Menu Item has become a hyperlink-less parent header, with the third level item as a link. Magento Menu Items are limited to three levels of hierarchy — you can’t go any deeper than this.
Choosing a Parent Menu
In the above examples, we showed you how to create your own top level Menu items. Before you do this in your own modules, its a good idea to ask yourself if your module really needs a new top level item. Using the Menu Item system, its possible to add your Menu Items to existing Magento core menu categories, and doing so avoids jamming up the admin with too many top level modules.
For example, if you change the following Menu Item
<!-- File: app/code/Pulsestorm/MenuTutorial/etc/adminhtml/menu.xml -->
<add id="Pulsestorm_MenuTutorial::third_level_example"
title="Third Level Example"
module="Pulsestorm_MenuTutorial"
sortOrder="9999"
resource="Magento_Backend::content"
parent="Pulsestorm_MenuTutorial::second_level_example"
action="cms/page/index"
/>
such that its parent points to a core Magento Menu Item instead
<!-- File: app/code/Pulsestorm/MenuTutorial/etc/adminhtml/menu.xml -->
<add id="Pulsestorm_MenuTutorial::third_level_example"
title="Third Level Example"
module="Pulsestorm_MenuTutorial"
sortOrder="9999"
resource="Magento_Backend::content"
parent="Magento_Backend::system"
action="cms/page/index"
/>
Your menu will appear under the top level System
Menu Item instead. There’s no clear guidance on where to put your module’s Menu Items — this is something every module developer will need to decide on their own.
If you need helping finding the id
values of Magento core Menu Items and are familiar with the unix command line, the following command pipeline may be of interest
$ find vendor/magento/ -name menu.xml | xargs grep 'title="System"'
Putting it Together with Pestle
We ran through creating a Menu Item manually as a teaching exercise. Fortunately, pestle
has a generate_menu
command that makes creating Menu Items simple. Here’s how add a new Menu Item to Magento’s own System -> Other Settings
menu with pestle.
First, lets use pestle to create a proper ACL rule for our Menu Item with the following.
$ pestle.phar generate_acl
Which Module? (Pulsestorm_HelloWorld)] Pulsestorm_MenuTutorial
Rule IDs? (Pulsestorm_MenuTutorial::top,Pulsestorm_MenuTutorial::config,)] Pulsestorm_MenuTutorial::menu_items,Pulsestorm_MenuTutorial::example_1
Created /path/to/magento2/app/code/Pulsestorm/MenuTutorial/etc/acl.xml
and then edit the generated acl.xml
to give our rules some titles.
<!-- File: app/code/Pulsestorm/MenuTutorial/etc/acl.xml -->
<resource id="Pulsestorm_MenuTutorial::menu_items" title="Tutorial Menu Items">
<resource id="Pulsestorm_MenuTutorial::example_1" title="First Example"/>
</resource>
If you’re not sure what the above does, checkout the Understanding Access Control List Rules article.
Next, we’ll use the generate_menu
command. Even if you’re familiar with pestle, you may want to read through our description of the interactive command line workflow below, as there’s a new pestle feature in play.
$ pestle.phar generate_menu
Module Name? (Pulsestorm_HelloGenerate)] Pulsestorm_MenuTutorial
Is this a new top level menu? (Y/N) (N)] N
Select Parent Menu:
[1] System (Magento_Backend::system)
[2] Dashboard (Magento_Backend::dashboard)
[3] System (Magento_Backend::system)
[4] Marketing (Magento_Backend::marketing)
[5] Content (Magento_Backend::content)
[6] Stores (Magento_Backend::stores)
[7] Products (Magento_Catalog::catalog)
[8] (Magento_Backend::system_currency)
[9] Customers (Magento_Customer::customer)
[10] Find Partners & Extensions (Magento_Marketplace::partners)
[11] Reports (Magento_Reports::report)
[12] Sales (Magento_Sales::sales)
[13] UMC (Umc_Base::umc)
()] 1
Use [Magento_Backend::system] as parent? (Y/N) (N)] N
Select Parent Menu:
[1] Report (Magento_Backend::system_report)
[2] Tools (Magento_Backend::system_tools)
[3] Data Transfer (Magento_Backend::system_convert)
[4] Other Settings (Magento_Backend::system_other_settings)
[5] Extensions (Magento_Integration::system_extensions)
[6] Permissions (Magento_User::system_acl)
()] 4
Menu Link ID (Pulsestorm_MenuTutorial::unique_identifier)] Pulsestorm_MenuTutorial::a_menu_item
ACL Resource (Pulsestorm_MenuTutorial::a_menu_item)] Pulsestorm_MenuTutorial::example_1
Link Title (My Link Title)] Look at me, I'm a link
Three Segment Action (frontname/index/index)] pulsestorm_menututorial/index/index
Sort Order? (10)] 9999
Writing: /path/to/magento2/app/code/Pulsestorm/MenuTutorial/etc/adminhtml/menu.xml
Done.
The first question,
Module Name? (Pulsestorm_HelloGenerate)] Pulsestorm_MenuTutorial
determines which module pestle will create a menu.xml
file in. The next set of questions
Is this a new top level menu? (Y/N) (N)] N
Select Parent Menu:
[1] System (Magento_Backend::system)
//...
[13] UMC (Umc_Base::umc)
()] 1
Use [Magento_Backend::system] as parent? (Y/N) (N)] N
Select Parent Menu:
[1] Report (Magento_Backend::system_report)
//...
[6] Permissions (Magento_User::system_acl)
()] 4
are an interactive workflow that let you create a new menu identifier, or add your menu to an existing menu. i.e. “Is this a parent
less item” and/or “what is this Menu’s parent”. Despite asking multiple questions, this is a single pestle argument (see below, and also the callback argument documentation)
The next question
Menu Link ID (Pulsestorm_MenuTutorial::unique_identifier)] Pulsestorm_MenuTutorial::a_menu_item
is where you enter the id
attribute to use in menu.xml
. Then there’s
ACL Resource (Pulsestorm_MenuTutorial::a_menu_item)] Pulsestorm_MenuTutorial::example_1
which is where you add the value used in the resource
attribute. The last two questions
Three Segment Action (frontname/index/index)] pulsestorm_menututorial/index/index
Sort Order? (10)] 9999
allow you to set the action
and sortOrder
attributes. With the above in place, reload your page, and your Menu Item can be found under System -> Other Settings
. If you’re interested in using a pestle one liner, that would be
$ pestle.phar generate_menu Pulsestorm_MenuTutorial Magento_Backend::system_other_settings Pulsestorm_MenuTutorial::a_menu_item Pulsestorm_MenuTutorial::example_1 "Look at me, I'm a link" pulsestorm_menututorial/index/index 9999
Notice how the entire interactive workflow to select a Menu Item ID is represented by one argument (Pulsestorm_MenuTutorial::a_menu_item
).
Wrap Up
That the basics of Menu Item creation in Magento 2. If you followed along with all the code examples, you may have noticed our Look at me, I’m a link Menu Item pointed to the following URL
http://magento.example.com/admin/pulsestorm_menututorial/index/index/key/ffcac30d7237841abc45a159390611334bb6665ce77cdf08f241e92f90688bda/
If you click on this link, Magento will redirect you to the Dashboard page. This happens because we have no URL endpoint setup for this URL! A Menu Item only generates our link. We still need to setup a MVC/MVVM endpoint for our page, which is what we’ll be doing next time!