Categories


Archives


Recent Posts


Categories


Magento 2: Admin MVC/MVVM Endpoints

astorm

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.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

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

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
Series Navigation<< Magento 2: Advanced Routing

Copyright © Alana Storm 1975 – 2023 All Rights Reserved

Originally Posted: 16th May 2016

email hidden; JavaScript is required