Magento 2 Book Review: Theme Web Page Assets

Like this article? Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Today’s a slight change of pace. We’re going to a review a new Magento 2 book, Theme Web Page Assets: Getting Stuff Done with Magento 2.

This book is part of Alan Kent’s Getting Stuff Done with Magento 2 series of ebooks, and the first book in the series with real substance behind it. If you’re not familiar with Alan’s work, he was Magento 2’s “lead architect” during the last bit of the eBay days, and took on the role of VP of Architecture when eBay spun Magento off as an independent company.

We mention all this not to put Alan’s career in the spotlight, but to highlight that fact that, despite not being a book that’s officially released by Magento Inc., (with an explicit disclaimer that its opinions are Alan’s alone), when a lead architect talks about the technical details of the platform he’s architecting, it’s wise to pay attention. While architect seems more of a management role than a hands on systems coding position, the leader of the technology team is bound to have some useful things to say about a platform.

For the impatient? The book’s worth buying, especially in these early days when varied knowledgeable opinions about Magento 2 are hard to find. It’s well thought out, well written, and takes the tried and true approach of telling you want you’re about to learn, teaching it to you, and then telling you what you learned.

Theme Web Page Assets: Getting Stuff Done with Magento 2 also takes a slightly different tact that I do. My not-so-secret goal here is to make you a better programmer, because programming is not as hard as it seems. While all my articles have a practical goal in mind, they also push you to think about how a system’s implemented, and realize that it’s just PHP code all the way down. The kernel level of my articles center around PHP and programming.

The kernel of Theme Web Page Assets: Getting Stuff Done with Magento 2 centers around HTML, CSS, and javascript. It’s written to teach you how Magento’s systems should be used, not how they’re implemented. This approach has its pros and cons, but it’s useful to look at any technical platform from multiple points of view.

What’s Covered

In short, when you pickup a copy of Theme Web Page Assets: Getting Stuff Done with Magento 2, you’ll end up learning about the following Magento specific topics

  • How to add a Theme Component
  • How to add a CSS File to a Theme
  • How to add a Block and Template to a page Footer Image
  • How to Replace an Existing base Theme Image
  • How Theme Files Resolve
  • Theme Inheritance
  • How to use Magento for Basic Responsive Web Design/Media Queries
  • Magento LESS CSS features
    • UI library
    • @magento_import
  • How to Clear Static Cache Files
  • How to Explore/Debug a UI feature to add CSS/Less
  • Basics of Using Layout Handle XML Files and Blocks
  • How to use Block Arguments
  • How to use Show Template Hints
  • PHP code in phtml Templates
  • Magento’s RequireJS basics
  • How to add a Magento Module to add javascript files and features
    • data-mage-init javascript initialization
    • text/x-magento-init javascript initialization
  • Magento’s gettext style __ function for locale-translating symbols
  • How to Add Fonts to a Theme
  • Basics of Submitting a Theme to Magento Connect

The book also, inevitably, touches on some general web development topics that aren’t specific to Magento.

  • Some CSS basics
  • CSS Optimization Trade Offs (merging vs. not, HTTP V1 vs. HTTP V2)
  • Introduction to Less CSS
  • Less CSS @import Statement
  • Less CSS mixins
  • Gulp workflows for CSS Generation
  • jQuery Widgets

The book is structured around a series of How To steps where Alan walks you through how to perform some task related to creating or editing a theme. While no example goes too in depth, they do give you that all important starting point that can be the difference between taking that first step, and throwing up your hands in frustration.

This book is also the first place I’ve seen some specific topics covered — I even managed to learn a thing or two about the data-mage-init and text/x-magento-init javascript embedding.

If the above sounds good to you and you’re working professionally in the Magento space, the book’s $9 price tag is well worth it.

What’s Not Covered

Theme Web Page Assets: Getting Stuff Done with Magento 2 is about a 100 “Kindle pages” long — that is, each page press on my Kindle marked the book done by another percent mark. Its short, to the point, and as a result there’s a few you won’t find.

First, and of greatest interest to the readers of this web site, there’s no coverage of how these features are implemented. The layout handle XML files are treated more as configuration files than the true domain specific language they are, and the only mention of PHP code is block class names and phtml templates. While the book never sets out to cover this, if you were hoping for some software architecture secrets from Magento’s lead architect, you’ll need to look elsewhere.

Second, the book offers no strong opinions how how or why you’d choose a particular theming approach over another. When to use CSS vs. Less is glossed over, as is the best times to use the data-mage-init javascript initialization over the text/x-magento-init javascript initialization method. As leader of an enormous software team, it’s probably not wise for a VP of Engineering to favor one solution over another, but a larger discussion of the tradeoffs involved or the thinking of the core team thinking around these features would be welcomed.

Finally, this book only show you how to get to the trailhead. That is, it puts the tools of Magento’s theming system in your hands, but its still going to be up to you to figure out what sections of Magento need theming, and which ones don’t. If you’re looking for that comprehensive guide to Magento theming, this book will give you the information you need to research your own, but it won’t provide an in depth guide for your front end developers to follow when building out a store theme. As always with ecommerce systems, that job is left to the men and women in the trenches, desperately avoiding meetings so they can get some work done.

Wrap Up

All in all Theme Web Page Assets: Getting Stuff Done with Magento 2 is a welcome addition to the growing body of knowledge on Magento 2 development. While it would be nice if Magento Inc. could find a way to allocate a few more architects and programmers to the Dev Docs team, this no nonsense guide to Magento 2 Theme Development will go a long way towards getting front end design-agency developers on track to working on their first Magento 2 project.

Originally published May 25, 2016

Magento 2: Advanced Routing

Like this article? Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Today we’re going to cover a few of the advanced features of Magento’s routing system, and discuss the history of some seemingly acute sharp edges. While all the techniques available in this article may not be the best way to achieve your goals, as a working Magento developer you’ll need to be aware they’re possible, and that other developers (including Magento’s core engineering team) may have used them.

This article assumes you have solid Magento 2 module experience, and it may gloss over details that are critical for a beginner. If that’s you, use the comments to ask your question or point to your Stack Exchange question.

Magento URL Action Strings

As a PHP programmer, when you need to create a URL in Magento, you use the getUrl method of a block or URL model object.

Mage::getUrl('foo/baz/bar');
$urlModel->getUrl('foo/baz/bar');
$this->getUrl('foo/baz/bar');

This is true in Magento 1 and Magento 2. In Magento 2 you don’t have access to the global Mage::getUrl method, so you’ll need to inject the specific URL model you want via automatic constructor dependency injection. URL models allow PHP programmers to treat URLs as a simple action string, and then have the core system code construct the final rendered URL. If you’ve only ever done front end work, this may seem like overkill, but it’s advantageous if the URL structure needs to change in the future or accommodate multiple contexts. Most web programming frameworks consider this a necessary feature.

We’re not going to dive too deeply into URL models today — what we are interested in is the action string these URL methods accept. At first glance, you may think the foo/baz/baz structure maps directly to the frontName/controllerName/actionName structure of a Magento URL. You’d almost be right. The actual structure is routeId/controllerName/actionName.

When you setup a routes.xml file for your module, you use an XML structure that looks like this

<!-- File: app/code/Package/Module/etc/[area]/routes.xml -->
<route id="routerId" frontName="urlFrontName">
    <module name="Package_Module" />
</route>

That is, you setup a <route/> node with an id and frontName attributes. The id identifies the route node uniquely in the Magento system, and the frontName defines the first segment of your URL.

When Magento generates a URL from an actions string like foo/baz/bar, it uses the first segment to lookup a <route/> node in the merged XML tree, and then use that route node’s frontName as the first URL segment.

This isn’t an obvious thing, and you may be developing on Magento for years without realizing it. That’s because most modules use route IDs and front names that are identical.

For example, you can see this in the catalog module

<!-- File: app/code/Magento/Catalog/etc/frontend/routes.xml -->
<route id="catalog" frontName="catalog">
    <module name="Magento_Catalog" />
</route>

both the id and frontName attributes are catalog. This convention also existed in Magento 1.

<!-- File: app/code/core/Mage/Catalog/etc/catalog.xml -->
<frontend>
    <routers>
        <catalog>
            <use>standard</use>
            <args>
                <module>Mage_Catalog</module>
                <frontName>catalog</frontName>
            </args>
        </catalog>
    </routers>
    <!-- ... -->
</frontend>

The above is Magento 1’s catalog router configuration. A single <catalog/> node under <routers/> configures a frontName named catalog. In Magento 2, that node under <routers/> has been turned in <module/>, with the id attribute replacing the Magento 1 node name. (i.e. <catalog/> becomes <module id="catalog"/>.

This convention makes it easy to look at a URL action string and get a rough idea of what the final URL will look like. However, there’s one huge exception: Magento’s admin URLs.

Magento Admin URLs

In Magento 1, the main admin route was configured with the following (in the Magento core)

<!-- File: app/code/core/Mage/Adminhtml/etc/config.xml -->
<admin>
    <routers>
        <adminhtml>
            <use>admin</use>
            <args>
                <module>Mage_Adminhtml</module>
                <frontName>admin</frontName>
            </args>
        </adminhtml>
    </routers>
</admin>

That is, the single node in <routers/> named <adminhtml/> sets up a frontName named admin. This carries over into Magento 2.

#File: vendor/magento/module-backend/etc/adminhtml/routes.xml 
<route id="adminhtml" frontName="admin">
    <module name="Magento_Backend" />
</route>

This means, in Magento 1, an action string like adminhtml/foo/bar will translate into a full URL that looks something like http://example.magento.com/admin/foo/bar. That is, the first URL segment, and front name, will be admin instead of adminhtml.

This is an odd exception to Magento’s general convention of matching front name and router id, and is likely less a deliberate design decision than it is Magento 1 launching with a half finished concept of areas (adminhtml, frontend) implemented.

Regardless, you can still find evidence of this non-decision in Magento 2’s codebase.

Magento 2 Admin URLs

In Magento 1, the adminhtml router ID is part of what ensured admin URLs began with the string /admin, and that the Magento area be set to adminhtml.

However, in Magento 2, every route setup in etc/adminhtml/routes.xml files will automatically be prepended with the string admin. This is what lets us use our own frontName in these adminhtml/routes.xml files.

This ends up having a curious side effect on legacy admin URLs that still use the adminhtml router ID. Consider the URL rewrite module

<!-- File: vendor/magento/module-url-rewrite/etc/adminhtml/menu.xml -->
<add id="Magento_UrlRewrite::urlrewrite" 
     title="URL Rewrites" module="Magento_UrlRewrite"
     sortOrder="20" 
     parent="Magento_Backend::marketing_seo"
     action="adminhtml/url_rewrite/index"
     resource="Magento_UrlRewrite::urlrewrite"
     />

The above menu.xml file uses an action string of adminhtml/url_rewrite/index. Magento ends up generating a URL like this

http://magento.example.com/admin/admin/url_rewrite/index

That’s a URL that begins with /admin/admin. That’s two admin strings. The first comes from the /admin URL segment that Magento prepends to every admin URL. The second comes from Magento using the adminhtml route ID to lookup a frontName attribute.

These appear to be legacy URLs automatically converted into menu items, although when I asked Magento’s architects about this back in January they were initially confused by the question, and then stated both admin URL formats (adminhtml route IDs/front names and custom route IDs/front names) were valid, but that custom route ID and front names were preferred.

Given the lack of clear initial rules, and the lack of the core team’s adherence to these later decreed rules, working Magento developers will want to be familiar with both URL formats, and be ready to debug them as needed.

URL Route Sharing

If you have not dug too deeply into Magento’s core code, you may be wondering

Wait — I thought each Magento module claimed a specific front name — how can multiple modules claim the admin front name via the adminhtml router?

This brings us to another feature from Magento 1 that made the jump to Magento 2: Route sharing.

Magento’s “1 module, 1 front name” policy traces its roots back to the Zend Framework’s early routing/MVC system. Magento 1, while a framework all its own, based a lot of its work on core Zend Framework classes, and this “1 module, 1 front name” feature came along for the ride. Module developers quickly noticed how limiting this was, and Magento introduced the ability for multiple modules to claim a particular front name.

If that didn’t make sense

  1. When Magento 1 was created, you could only create Controller files for a URL starting with /foo/... in a single module

  2. Magento introduced the ability for multiple modules to have controller files for URLs starting with /foo/ in Magento 1.3

The ability persists in Magento 2, and syntax for it has been greatly simplified. For example, if you wanted to go back to the first module in this series (Pulsestorm_HelloWorldMVVM) and add a front end URL endpoint at catalog/foo/bar, all you’d need to do is add the following configuration

<!-- File: app/code/Pulsestorm/HelloWorldMVVM/etc/frontend/routes.xml-->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="hello_mvvm" frontName="hello_mvvm">
            <module name="Pulsestorm_HelloWorldMVVM"/>
        </route>

        <!-- START: new configuration -->        
        <route id="catalog">
            <module name="Pulsestorm_HelloWorldMVVM" after="Magento_Catalog"/>
        </route>
        <!-- END:   new configuration -->                
    </router>
</config>

and the following controller file.

#File: app/code/Pulsestorm/HelloWorldMVVM/Controller/Foo/Bar.php
<?php    namespace Pulsestorm\HelloWorldMVVM\Controller\Foo;
class Bar extends \Magento\Framework\App\Action\Action
{    
    public function execute()
    {
        var_dump("Proof of life");
    }
}

with the above in place, you now have a second module with controller files for the catalog front name, and Magento will use your controller when you load the catalog/foo/bar URI.

Unlike Magento 1, the configuration for this is almost identical to setting up routing for a single module

<!-- File: app/code/Pulsestorm/HelloWorldMVVM/etc/frontend/routes.xml-->
<route id="catalog">
    <module name="Pulsestorm_HelloWorldMVVM" after="Magento_Catalog"/>
</route>

The two main differences are

  1. You do not configure a frontName attribute in the <route/> tag

  2. You need (or are strongly advised) to use a before or after tag to control the order Magento will check for matches in

When you use the above configuration, you’re telling Magento

Hey, you know that <route/> tag with an id of catalog? I want you to merge in an extra <module/> node.

When Magento encounters multiple <module/> nodes in its global configuration, it will check each module for a controller match until it finds one. Using the after tag above ensures Magento checks our module after the Magento_Catalog module. Without this, it would be possible for us to accidentally create a controller file that replaced the core controllers in vendor/magento/module-catalog/Controller.

It’s important to note that you’re looking for the <route/> id attribute, and not the frontName attribute here. For example, when a module wants to add to the admin front name, a Magento core developer will

  1. Identify the module that first added the frontName="admin"
  2. Identify that <module/>’s <route/> id
  3. Use that ID in their own modules
  4. Ensure a proper before or after tag is in place

So, step one — the module that initially added the admin front name is Magento_Backend

#File: vendor/magento/module-backend/etc/adminhtml/routes.xml 
<route id="adminhtml" frontName="admin">
    <module name="Magento_Backend" />
</route>

This <route/> node’s id is adminhtml. So, the other Magento modules that use the admin front name configure themselves with id="adminhtml".

<!-- File: vendor/magento/module-variable/etc/adminhtml/routes.xml -->
<route id="adminhtml">
    <module name="Magento_Variable" before="Magento_Backend" />
</route>

<!-- File: vendor/magento/module-widget/etc/adminhtml/routes.xml -->
<route id="adminhtml">
    <module name="Magento_Widget" before="Magento_Backend" />
</route>    

Default Action String Segments

Before we move on from advanced routing, there’s a few last things to mention. You may occasionally see an action string that’s missing its second or third segment.

<!-- File: vendor/magento/module-tax/etc/adminhtml/menu.xml -->
<add 
    id="Magento_Tax::sales_tax_rules" 
    title="Tax Rules" 
    module="Magento_Tax" 
    sortOrder="10" 
    parent="Magento_Tax::sales_tax" 
    action="tax/rule" 
    resource="Magento_Tax::manage_tax"/>

When Magento encounters an action string with a missing segment, it will substitute the string index. In other words, the action string of tax/rule used above is functionally equivalent to an action string of tax/rule/index.

Also, in PHP code, you may occasionally see the * character in a URL action string.

$this->getUrl('*/*/*')

These asterisks will be translated as the current front name, controller name, or action name. In other words, they create context dependent URLs, and are useful in base UI classes meant to be used in multiple modules.

Wrap Up

Like a lot of the “in the trenches” decisions made by Magento 2’s non-architects (i.e. the programmers actually implementing the features), it’s not 100% clear why these admin/admin URLs stuck around. As third party developers, it’s probably best for us to create our own URL front names, and only rely on the admin front name if there’s some feature that makes it absolutely necessary.

Originally published May 23, 2016

Magento 2: Admin MVC/MVVM Endpoints

Like this article? Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Now that we know how to create Access Control List Rules, and how to generate Admin Menu Items with Magento 2’s CSFR protection, we’re ready to create an MVC/MVVM URL endpoint (i.e. an HTML page) in Magento 2’s admin.

While you’ll find useful information here without it, we’re assuming a basic familiarity with creating routes on Magento’s front end. If there’s a concept below that confuses you, work through the first article in this series before linking to your question on the Magento Stack Exchange in the comments.

Recreate Previous Progress with Pestle

Before we start, we’ll need a module with a few predefined ACL rules, and an admin Menu Item. We’re going to use pestle to quickly get a module up and running. See the previous articles in this series if you’re not sure what the following commands do.

After running the following five commands

$ pestle.phar generate_module Pulsestorm HelloAdminBackend 0.0.1

$ pestle.phar generate_acl Pulsestorm_HelloAdminBackend Pulsestorm_HelloAdminBackend::top,Pulsestorm_HelloAdminBackend::menu_1

$ pestle.phar generate_menu Pulsestorm_HelloAdminBackend Magento_Backend::system_other_settings Pulsestorm_HelloAdminBackend::a_menu_item Pulsestorm_HelloAdminBackend::menu_1 "Hello Admin Backend Pestle" pulsestorm_hello_admin_backend/index/index 1

$ php bin/magento module:enable Pulsestorm_HelloAdminBackend

$ php bin/magento setup:upgrade

you’ll have a Pulsestorm_HelloAdminBackend module with an ACL hierarchy defined, and a single Menu Item under System -> Other Settings -> Hello Admin Backend Pestle. With that, we’re ready to get started.

Magento 2 Admin URL Structure

If you look at the URL of your generated Menu Item hyperlink (Right click, Copy Link As)

http://magento.example.com/admin/pulsestorm_hello_admin_backend/index/index/key/...

you’ll see Magento 2’s backend URLs have a four segment URL structure. All Magento 2 admin urls start with the segment /admin/. This sets the area code for the backend. The next three segments

pulsestorm_hello_admin_backend/index/index

are your module front name (pulsestorm_hello_admin_backend), the controller name (index) and the action name (index). For now, just make a note of these, as we’ll be referencing them below.

Create Files

We’re going to start by using pestle to create a routes.xml file and controller file for our URL. Run the following command

$ pestle.phar generate_route  
Which Module? (Pulsestorm_HelloWorld)] Pulsestorm_HelloAdminBackend
Which Area (frontend, adminhtml)? (frontend)] adminhtml
Frontname/Route ID? (pulsestorm_helloworld)] pulsestorm_hello_admin_backend

and pestle will add two files to your system

app/code/Pulsestorm/HelloAdminBackend/etc/adminhtml/routes.xml
app/code/Pulsestorm/HelloAdminBackend/Controller/Adminhtml/Index/Index.php

Clear your cache and generated class files, and then click on the Hello Admin Backend Pestle menu. If you’re logged in as the Magento super user, you should be brought to a new, blank admin page!

We’ll get to why this doesn’t work with a non super-user in a moment.

The generate_route command accepts three arguments. The first (Pulsestorm_HelloAdminBackend) is the name of the module you want to add your routes.xml to. The second, (adminhtml) is the Magento area to use for your route. The area normally defaults to frontend, but here we’ve told Magento we want to create a route and controller file for the adminhtml area. The final argument, (pulsestorm_hello_admin_backend), is the frontname to use for our URLs. This serves the same purpose as a front end front name, but with a few key differences we’ll get to in a moment.

First though, let’s take a look at the generated routes.xml file.

Magento 2 adminhtml Routes

The first thing to make a note of is the final path of routes.xml

#File: app/code/Pulsestorm/HelloAdminBackend/etc/adminhtml/routes.xml

You’ll notice Magento wants this file in the adminhtml sub-folder of etc. That’s because we’re setting up a route for the backend admin area. If we open this file

<!-- File: app/code/Pulsestorm/HelloAdminBackend/etc/adminhtml/routes.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="admin">
        <route id="pulsestorm_hello_admin_backend" frontName="pulsestorm_hello_admin_backend">
            <module name="Pulsestorm_HelloAdminBackend"/>
        </route>
    </router>
</config>

we’ll see a familiar structure. A routes.xml file is a collection of <router/> nodes. Under the <router/> nodes you’ll find individual <route/> nodes. Each of these <route/> nodes tells Magento that a particular URL front name is claimed by a particular module. Above, we’ve told Magento the module Pulsestorm_HelloAdminBackend claims the front name pulsestorm_hello_admin_backend.

Compared to the front end, there are two major differences to be aware of in Magento backend routing. First, the top level <router/> node does not use the standard id.

<router id="admin">

Instead, all Magento 2 admin backend <router/> tags use an id of admin.

The second thing to be aware of is, as previously mentioned, a backend admin URL’s front name (pulsestorm_hello_admin_backend) is actually the second segment of a URL.

http://magento.example.com/admin/pulsestorm_hello_admin_backend/index/index/key/...

All Magento’s backend URLs have /admin as the first segment of a URL. The front name is the second segment. This has some implications for legacy backend routes and Magento’s internal URL identifiers, which we’ll cover in a later article. For now though, let’s take a look at the generated controller file.

Magento 2 Backend Controllers

The first thing you’ll notice about Magento 2 admin controllers is their file names

app/code/Pulsestorm/HelloAdminBackend/Controller/Adminhtml/Index/Index.php

Like all Magento 2 PHP class files, a controller file’s path and name are based on the full class name of the controller class. Magento front end controller’s use a [Package]\[Module]\Controller\[Controller Segment]\[Action Segment] naming convention. Magento 2 backend controller files use a similar naming convention — the only difference is an additional Adminhtml segment.

[Package]\[Module]\Controller\Adminhtml\[Controller Segment]\[Action Segment]

In our case, our controller class name is Pulsestorm\HelloAdminBackend\Controller\Adminhtml\Index\Index. When you create a route with pestle, pestle automatically assumes you want an [frontname]/index/index URL, and creates the controller class name with a corresponding Index\Index.

If you wanted a different URL (say pulsestorm_hello_admin_backend\foo\baz), then you’d manually create a Pulsestorm\HelloAdminBackend\Controller\Adminhtml\Foo\Bar class.

There’s more differences inside a backend controller’s class definition. Let’s take a look

#File: app/code/Pulsestorm/HelloAdminBackend/Controller/Adminhtml/Index/Index.php
<?php
namespace Pulsestorm\HelloAdminBackend\Controller\Adminhtml\Index;
class Index extends \Magento\Backend\App\Action
{
    protected $resultPageFactory;
    public function __construct(
        \Magento\Backend\App\Action\Context $context,
        \Magento\Framework\View\Result\PageFactory $resultPageFactory)
    {
        $this->resultPageFactory = $resultPageFactory;        
        return parent::__construct($context);
    }

    public function execute()
    {
        return $this->resultPageFactory->create();  
    }    
    protected function _isAllowed()
    {
        return $this->_authorization->isAllowed('ACL RULE HERE');
    }            

}

There’s a few things to make note of here. First, all Magento backend controllers need to inherit from the Magento\Backend\App\Action class. Not doing so may introduce instability into the system, and open up unintended privacy and security leaks.

The next thing to make note of is the __construct method. While you don’t need a __construct method in your controller, if you do use one you’ll need to include a backend context object

\Magento\Backend\App\Action\Context $context

and make sure you pass that object as the first argument when calling the parent constructor.

return parent::__construct($context);

Context objects are beyond the scope of this article, but in short they’re a cheat the Magento core team uses to inject multiple objects via automatic constructor dependency injection without having multiple constructor arguments. If you didn’t understand that, don’t worry. Just make sure if you use a __construct method, that you follow the above advice.

The last thing to make note of w/r/t to Magento backend admin controllers is the _isAllowed method.

#File: app/code/Pulsestorm/HelloAdminBackend/Controller/Adminhtml/Index/Index.php

protected function _isAllowed()
{
    return $this->_authorization->isAllowed('ACL RULE HERE');
}       

The _isAllowed method is required, and is where you, as the module developer, define which ACL rule a user needs to access this URL endpoint. If _isAllowed returns true, then the user is allowed. If not, they’ll get an access denied screen. By convention, this method uses the object in the _authorization property to check a specific ACL rule. The sole exception to this is any account with super user rights. For super users, the results of _isAllowed are ignored.

The pestle generate_route command does not insert a rule for you automatically — that’s why you see the text ACL RULE HERE above. This is why our route only worked for Magento admin super users. If you edit this file to include the ACL rule we generated with generate_acl

protected function _isAllowed()
{
    return $this->_authorization->isAllowed('Pulsestorm_HelloAdminBackend::menu_1');
}       

then any user that’s assigned the Pulsestorm_HelloAdminBackend::menu_1 permission (via System -> Permissions) will be able to access this controller endpoint. By convention, this rule should be the same as the rule for accessing the Menu Item, but it’s up to each individual module developer to decide if they want to enforce this convention.

Create View Files

Now that we’ve created an admin endpoint, you’re probably wondering how to add content to it. Like the front end, Magento’s backend is controlled by Magento’s domain specific language for layouts. Below we’re going to use pestle to add a layout handle XML file, as well as a block and template.

$ pestle.phar generate_view
Which Module? (Pulsestorm_HelloGenerate)] Pulsestorm_HelloAdminBackend
Which Area? (frontend)] adminhtml
Which Handle? (pulsestorm_helloadminbackend_index_index)] pulsestorm_hello_admin_backend_index_index
Block Name? (Main)] Main
Template File? (content.phtml)] content.phtml
Layout (ignored for adminhtml) ? (1column)] 

After running pestle with the above arguments and clearing your cache, reload your page and you should see pestle’s default block content.

The main pestle difference, (compared to the front end), is the Which Area? argument — you’ll need to specify adminhtml.

This command creates three files — a layout handle XML file, a block view file, and a phtml template for the block. First, let’s take a look at the layout handle XML file.

<!-- File: app/code/Pulsestorm/HelloAdminBackend/view/adminhtml/layout/pulsestorm_hello_admin_backend_index_index.xml -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <referenceBlock name="content">
        <block  template="content.phtml" 
                class="Pulsestorm\HelloAdminBackend\Block\Adminhtml\Main" 
                name="pulsestorm_helloadminbackend_block_main" />
    </referenceBlock>
</page>

You’ll see the format is nearly the same as a layout handle XML file for the front end. The main difference is pestle creates this file in the view/adminhtml folder instead of view/frontend folder. The view/adminhtml folder is where Magento looks for layout handle XML files for backend admin console requests. Keen observers will also notice pestle does not add a layout="..." attribute to the top level page tag. While the backend does support this attribute for setting different page level layouts, most Magento backend layout handle files omit it.

This layout handle XML file adds a block whose class is Pulsestorm\HelloAdminBackend\Block\Adminhtml\Main, and whose template is content.html. If we look at the class generated for that block

#File: app/code/Pulsestorm/HelloAdminBackend/Block/Adminhtml/Main.php 
<?php
namespace Pulsestorm\HelloAdminBackend\Block\Adminhtml;
class Main extends \Magento\Backend\Block\Template
{
    function _prepareLayout(){}
}

we’ll see two main differences from a front end block. First, by convention, and admin block’s class name includes Adminhtml. While this isn’t strictly necessary, most of Magento uses this convention, and it never hurts to follow along with what the platform owner is doing. Second, and more importantly, admin blocks need to inherit from the base Magento\Backend\Block\Template class. This class gives admin block objects a different set of injected dependencies (a form key helper, an authorization helper, etc), and a different set of helper methods. While covering these differences is beyond the scope of this article, if you’re curious, take a look at the backend block’s class definition file.

#File: vendor/magento/module-backend/Block/Template.php

Finally, if we take a look at the generated phtml template file.

#File: app/code/Pulsestorm/HelloAdminBackend/view/adminhtml/templates/content.phtml
<h1>This is my template, there are many like it, but this one is mine.</h1>

We see a standard Magento 2 phtml template file. The only difference from a front end template is, again, that we create this file in the adminhtml folder.

At this point, all standard Magento 2 block and template programming techniques apply, and you can build out your admin console page however you want.

Setting the Current Menu and Page Title

There’s two last things we need to cover before moving on. You may have noticed that while navigating the admin, Magento will highlight the left side Menu Item of the page you’re currently on (Dashboard below)

This isn’t the case for our Menu Item. The System menu does not have a highlight. You also may have noticed that our page has a generic Magento Admin title.

There’s two additional steps you’ll need to take in your controller action to match the behavior of the core system. For the menu highlight, you’ll need to use the setActiveMenu method of the page object

#File: app/code/Pulsestorm/HelloAdminBackend/Controller/Adminhtml/Index/Index.php    

public function __construct(
    \Magento\Backend\App\Action\Context $context,
    \Magento\Framework\View\Result\PageFactory $resultPageFactory)
{
    $this->resultPageFactory = $resultPageFactory;        
    return parent::__construct($context);
}
//...    
public function execute()
{
    $page = $this->resultPageFactory->create();  
    $page->setActiveMenu('Pulsestorm_HelloAdminBackend::a_menu_item');
    return $page;
}    

The setActiveMenu method accepts a single string parameter — the name of the Menu Item you want to set as active. When you add this code, Magento (at the time of this writing) will do two things. The Menu Item’s top level parent (System) will be highlighted and the page’s default title will be set to second level Menu Item’s title (Other Settings).

If you’d like to set a different title, you can do so my directly manipulating the page object’s configuration.

#File: app/code/Pulsestorm/HelloAdminBackend/Controller/Adminhtml/Index/Index.php    
public function execute()
{
    $page = $this->resultPageFactory->create();  
    $page->setActiveMenu('Pulsestorm_HelloAdminBackend::a_menu_item');
    $page->getConfig()->getTitle()->prepend(__('Our Custom Title'));
    return $page;
} 

You’ll notice we’ve wrapped our string in the global “translate this symbol” function (__). This ensures our title is available to be translated into other languages. With the above in place, you’ll see your page title reflected in the browser.

If you’re thinking the syntax and behavior of these features is a little odd, I’m right there with you. The following

  • The setActiveMenu method is only available if we inject an object (vs. being something available directly on the backend controller
  • That setActiveMenu sets a page title, but not the actual page title of a third level menu
  • That the prepend method seems to set a title, but the available set method does not

all point to the backend controller features being unfinished. You’ll also see core controller actions that look more like this

#File: vendor/magento/module-user/Controller/Adminhtml/Locks/Index.php
public function execute()
{
    $this->_view->loadLayout();
    $this->_setActiveMenu('Magento_User::system_acl_locks');
    $this->_view->getPage()->getConfig()->getTitle()->prepend(__('Locked Users'));
    $this->_view->renderLayout();
}

This pattern (loadLayout, renderLayout) may look familiar to Magento 1 developers. Although this technique is present in the core, the _ leading variables are a hint (although not a guarantee) that the developers don’t intend to keep this technique around. Completing/finalizing these features and deciding on one path forward was likely dropped in favor of more visible functionality for the initial release of Magento 2.0. Such is the life of a software developer under the regime of architects and investors.

Wrap Up

With the above techniques you should be able to start building out your admin features. If you’re still having trouble, this Magento Quickies tutorial has some controller action debugging tips that apply to the front and backends.

All that said, we’ve only touched the surface of what’s happening in Magento’s backend. Next time, we’ll cover some advanced backend routing and layout topics, and prepare ourselves for a deeper dive into Magento’s backend UI functionality.

Originally published May 16, 2016