Magento 2 Tutorials

Building a Magento 2 Backend Launcher

Magento 2 Backend Launcher

It’s about time we take the bull by the horns and develop a Magento 2 extension. In this guide-through, we will take a stab at porting PulseStorm‘s Launcher to the Magento 2 backend.

Introduction

PulseStorm Launcher Interface

PulseStorm Launcher in Magento 1.X.

For those unfamiliar with the concept, it’s highly recommended to read Alan Storm’s introduction (or watch the video) of the original PulseStorm Launcher which we took inspiration from. The code is available on Github. It’s basically a neat way to quickly jump through the labyrinth of the Magento backend interface.

In this tutorial we will guide you through the development of such an extension for the Magento 2.X platform. The plan is to begin with the minimum viable functionality and to expand it in future postings.

In case you do not already have a Magento 2 installation, you can do so with this quick tip, or follow Mr. Bezhashvyly his more thorough instructions.

Magento 2 documentation exists, but it’s still a bit sparse. A bit of a disclaimer is thus that the work done is sometimes experimental or that there might a better way to do it which we are unaware of. If you have any input, please let us know in the comments.

Getting started

Glaring at the directory listing of your bleeding-edge Magento 2 installation, you might notice some resemblance with Magento 1.X. If you instinctively thought we would kick things off in the app/code directory, you are correct.

An immediate difference is that app/code no longer manifests code pools like community and core. Modules are now grouped directly by vendor. This means that next to the existing Magento directory, we will add a Magenticians vendor module. Within a vendor directory, the actual modules live, so we will create a Launcher directory in that.

What is also new, is that defining the existence of a moudule in app/etc/modules/[Vendor]_[Module].xml is no longer practiced. This is now done directly in the module.xml file in the etc directory of the module its root. Let’s create that file:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd">
    <module name="Magenticians_Launcher" version="0.0.0.1" active="true" />
</config>

As overheard in the Magento 2 front-end webinar, Magento 2 ships with schema defintions for recurring XML files. Note that XML files will be interpreted without referencing to these schemas, but adhering to standards — and receiving IDE hinting! — is always a good thing.

Going to the Advanced > Advanced section of the system configuration, you can verify that the module has been indeed recognized by the Magento system.

Inventorizing what we need

The module currently does close to nothing so let’s make a list of what we need to improve:

  • Inlcude assets (JS, CSS) to handle the launcher its functionality and appearance
  • Include an interface element somewhere to be able to spawn the launcher
  • Feed entries to the launcher the user can search through

Let’s get to it.

Including assets

Instead of having assets scattered through various potential directories, Magento 2 modularizes the concept of assets by sticking them with the module. Those assets are placed within the view/[areacode]/web directory of the module its root.

Because we solely want the launcher to operate in the admin panel area, we have to put the view files in the appropriate directory: view/adminhtml.

In each view-directory of an area code, you will generally find three sub-directories: web for assets, templates for the *.phtml files and layout for the XML configuration files. Let’s create those directories.

As the web directory hosts assets, the to-be-developed JavaScript and CSS files are put here. You can throw them in there straight away but it’s good manner to categorize them in a web/css and web/js directory.

We call the JavaScript-file we add launcher.js and the CSS-file launcher.css. In the launcher.js file we put a mundane console.log(‘I am here!’); statement so we can later quickly verify whether the file has been properly injected.

The launcher.css file stays empty for now.

Summoning them on a page

Now we have the assets available for inclusion, we can start writing some XML to get them included on every page. At this point you could manually scout through the Magento 2 code base and find a starting point, or keep your fingers crossed and hope it has already been provided in the documentation.

We are in luck, because there is documentation on the subject. Those instructions go into the default.xml file within the layout directory:

<?xml version="1.0"?>
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../Magento/Core/etc/layout_single.xsd">
    <referenceContainer name="head">
        <block class="Magento\Theme\Block\Html\Head\Script" name="magenticians-launcher-launcher-js">
            <arguments>
                <argument name="file" xsi:type="string">Magenticians_Launcher::js/launcher.js</argument>
            </arguments>
        </block>

        <block class="Magento\Theme\Block\Html\Head\Css" name="magenticians-launcher-launcher-css">
            <arguments>
                <argument name="file" xsi:type="string">Magenticians_Launcher::css/launcher.css</argument>
            </arguments>
        </block>
    </referenceContainer>
</layout>

Verifying that everything went correct: refresh the admin panel and check the browser its console, it should display the “I am here!” line added a tad ago.

Include an interface element

Magento 2 tray

This is where we will attempt to add the interface element.

With the interface element we need to be able to launch the dialog of the launcher. We could add an menu entry and have it function as a button, but a more semantic solution would be to add it to the admin panel tray as seen on the right.

To put a block there, we will need to find the name of the container we have to reference. After enabling template hinting, we can use any existing template as a starting point to find out where and how it gets added.

Picking the toolbar_entry.phtml template – used for displaying notifications – we simply go to the Magento/AdminNotification module and search through the view/adminhtml/layout directory for a reference of the container. It’s found quickly in layout/adminhtml/default.xml:

<referenceContainer name="header">
    <block class="Magento\AdminNotification\Block\ToolbarEntry" before="user" template="toolbar_entry.phtml" />
</referenceContainer>

The XML instructions are straight forward and comparable to how it’s done in Magento 1.X. We only have to adapt this example and add it to the module its default.xml:

<referenceContainer name="header">
    <block class="Magento\Framework\View\Element\Template" before="search" name="launcher_toolbar_entry" template="Magenticians_Launcher::toolbar_entry.phtml" />
</referenceContainer>

Instead of creating an underlying block class from scratch, we will use a standard internal Magento view element which will render the template for us. Logically, the toolbar_entry.phtml goes in the templates directory. Let’s fill it in.

A simple “+”

For the interface element we will use a simple plus-sign styled in the appropriate size and color. If you have anything more creative in mind, feel free to do so:

<div id="magenticians-launcher-toolbar">
    <a id="magenticians-launcher-link" href="javascript:void(0);">+</a>
</div>

And the accompanying CSS which goes in launcher.css. Note that we also prematurely add the minimal styling for the launcher its interface:

#magenticians-launcher-toolbar {
    display: inline-block;
    font-weight: bold;
    font-size: 31px;
    vertical-align: top;
}
#magenticians-launcher-toolbar > a {
    color: #fff;
}
#magenticians-launcher-toolbar > a:hover {
    color: #ccc;
    text-decoration: none;
}
#magenticians-launcher-dialog {
    display: none;
}
#magenticians-launcher-input {
    width: 100%;
}

There we go. Refreshing the admin panel should now reveal a “+”-sign to the left of the search-icon. For those paying attention, you will notice it is at the wrong side. What’s wrong?!

A problem of sequence

sequence problem Magento 2

Note actual order of the modules might be different – ours simply isn’t on time.

As illustrated above, the search bar, which comes with the Magento_Backend module, does not yet exist when a block of the launcher module demands to be put before it.

This is simply solved by telling Magento that the module should be initiated after Magento_Backend. Because Magento_Backend is also loading jQuery and various other fundamental dependencies, it’s a good idea to always sequence your module after Magento_Backend.

Module Dependency Declarations are placed in the module.xml file:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd">
    <module name="Magenticians_Launcher" version="0.0.0.1" active="true">
        <sequence>
            <module name="Magento_Backend" />
        </sequence>
    </module>
</config>

With this change, refreshing the admin panel should now reveal the interface element on its proper position.

We need another block

Though we now have an interface element to boot the launcher with, we will need another block for rendering the dialog and which later on will also function as the portal between the entries coming from Magento and the JavaScript which makes the launcher work.

Because this block needs some underlying bussiness logic, we will have to use a supporting class. Blocks find their place in the Block directory of a module its root. We will name the block LauncherItems so naturally it goes in Block/LauncherItems.php.

For now, this block only has to function as renderer so we will give it a $_template property and have it extend the appropriate class:

<?php
namespace Magenticians\Launcher\Block;

class LauncherItems extends \Magento\Backend\Block\Template
{
    protected $_template = 'launcher.phtml';
}

That will do the trick. To have it render on the page, we will have to add an entry to layout/default.xml:

    [...]
    
    <referenceContainer name="before_body_end">
        <block class="Magenticians\Launcher\Block\LauncherItems" name="launcher_items" />
    </referenceContainer>
</layout>

Because the position of the dialog is irrelevant, we’ll throw the block in the before_body_end container. That container its location shouldn’t be a surprise.

Just like before, we will not use anything fancy interface-wise. Just a simple text input should do the trick:

** Magenticians/Launcher/view/adminhtml/templates/launcher.phtml **
<?php
/**
* @var $this Magenticians\Launcher\Block\LauncherItems
*/
?>

<script type="text/javascript">
var launcher_items = {};
</script>

<div id="magenticians-launcher-dialog" title="Launcher">
    <input placeholder="Awaiting command." type="text" id="magenticians-launcher-input" />
</div>

As you can see we have defined a global launcher_items variable. Empty for now, it will soon contain all the entries which the launcher will use.

The JavaScript

Now that we have all the interface elements of the launcher set up, we have to actually build it. Looking back at the PulseStorm Launcher there is a lot of custom JavaScript. Though it might be fun to adapt that or write all the functionality from scratch, we have one big advantage now that we are working with Magento 2.

jQuery UI is already here

That’s right, all of the functionality we will need can come from the jQuery UI library which is already available on every Magento backend page. Due to this advantage we can make use of these jQuery UI widgets:

  • Dialog: for the dialog containing the launcher
  • Autocomplete: for the launcher its search function

Some might argue this is “lazy” or call us out on using the “bloated” jQuery (UI) library. But, why bother reinventing the wheel and refusing viable implementations which are already within hands reach?

Though gifted with most most of the work done, there is still a bit of JavaScript we will have to write ourselves. We will start with actually launching the launcher its interface when the appropriate element is clicked:

jQuery(document).ready(function($) {
    $('#magenticians-launcher-link').click(function() {
        $('#magenticians-launcher-dialog').dialog({
            width: 500
        });
    });
}

Going back to the admin panel and clicking the “+” in the tray, the dialog which we defined will greet you. Because the input field in the dialog doesn’t do anything yet, let’s continue.

To make the input field work, we will use jQuery UI Autocomplete. It’s really simple:

$('#magenticians-launcher-input').autocomplete({
    source: launcher_items
});

Because the launcher_items we defined in launcher.phtml is still empty, let’s tinker about the best format we can define the entries in.

Looking at the documentation there are quite a few types the source option supports. The most fitting one for us, is using an array of objects with label and value properties.

This way we can define the labels of the entries we can search through and use the value to store the URL that entry “launches”:

$('#magenticians-launcher-input').autocomplete({
        source: launcher_items,
        
        // On selecting an entry, we point the document to the location attached to it
        select: function(event, ui) {
            event.preventDefault();
            document.location = $(this).attr('data-target');
        },
        
        // When an entry receives focus we display the label in the input and store the URL in "data-target"
        focus: function(event, ui) {
            event.preventDefault();
            $(this).val(ui.item.label);
            $(this).attr('data-target', ui.item.value);
        },
        
        // When the autocomplete widget is initialized, we quickly remove the accessible helper as we don't need it
        create: function(event) {
            $(this).next('.ui-helper-hidden-accessible').remove();
        }
});

To test it out, simply define an array in the specified format and temporarily add it to launcher.html its launcher_items:

var launcher_items = [{label: "Homepage", value: "http://magenticians.com"}, {label: "@Magenticians on Twitter", value: "http://twitter.com/magenticians"}, {label: "Get in touch", value: "http://magenticians.com/contact"}];

magenticians launcher demo with dummy data

The launcher at work with some dummy data.

Refresh the admin panel, boot the launcher and start typing to find out whether it works properly. Now that everything is working client side, we can continue with the final step: feeding the desired entries from Magento.

The menu entries

Though the original PulseStorm Launcher also has the capability to search through Magento configuration sections, we will start with only the menu entries for now. As stated in the introduction, we hope to expand the launcher its functionality in the future.

The easiest way to find out where we can obtain the menu entries from, is to turn template hinting back on and inspect the block class which is responsible for rendering the menu.

template hinting Magento 2 backend

As seen as above, it is the Magento/Backend/view/adminhtml/templates/menu.phtml template which is responsible for rendering the menu. The underlying block class is Magento\Backend\Block\Menu.

** Magento/Backend/view/adminhtml/templates/menu.phtml ** 

<nav class="navigation">
    <?php echo $this->renderNavigation($this->getMenuModel(), 0, 12); ?>
</nav>

The renderNavigation method, as its name implies, renders the navigation. It’s a bit nasty that a lot of HTML is being generated within a core class, but we will look over that. It does however show us how we can obtain the menu entries:

/** @var $menuItem \Magento\Backend\Model\Menu\Item  */
foreach ($this->_getMenuIterator($menu) as $menuItem) { [...]

The iterator is retrieved from the iterator factory:

protected function _getMenuIterator($menu)
{
    return $this->_iteratorFactory->create(array('iterator' => $menu->getIterator()));
}

$menu is an instance of Magento\Backend\Model\Menu. The renderNavigation method is initially fed the root menu of the current menu configuration ($this->getMenuModel() in menu.phtml). Then, it gets iterated over recursively.

Most of the Menu block class its methods are protected. Though we can repurpose a few of the methods (like $this->getMenuModel()), some of the code has to be duplicated to the LauncherItems block. You could choose to extend the Menu-block, but as Menu and LauncherItems are completely different, it wouldn’t really make hierarchical sense.

Editing the mostly empty LauncherItems block class created a while ago, we will start with defining the dependencies. Magento will take care of injecting them for us:

<?php
namespace Magenticians\Launcher\Block;

class LauncherItems extends \Magento\Backend\Block\Template
{
    protected $_template = 'launcher.phtml';
    protected $_iteratorFactory;
    protected $_blockMenu;

    public function __construct(
        \Magento\Backend\Block\Template\Context $context,
        \Magento\Backend\Model\Menu\Filter\IteratorFactory $iteratorFactory,
        \Magento\Backend\Block\Menu $blockMenu,
        array $data
    ) {
        $this->_iteratorFactory = $iteratorFactory;
        $this->_blockMenu = $blockMenu;
        $this->_url = $url;
        parent::__construct($context, $data);
    }
]

Now we have the dependencies available, we can reuse the Menu block its getMenuModel() method:

public function getMenuModel()
{
    return $this->_blockMenu->getMenuModel();
}

Because the _getMenuIterator method is protected, we have to redefine it in our class:

protected function getMenuIterator($menu)
{
    return $this->_iteratorFactory->create(array('iterator' => $menu->getIterator()));
}

Now we have those two methods in place, we can recursively iterate over the menu. Don’t be spooked by the recursion part, it’s a lot simpler than it sounds: we will start iterating over the root elements. Then, if a root item has children, we will iterate over its children. And so forth. It’s not that complex.

We will call the method which does this getMenuArray:

public function getMenuArray($menu, & $result = array())
{
    foreach ($this->getMenuIterator($menu) as $menuItem) {
        /** @var $menuItem \Magento\Backend\Model\Menu\Item  */
        $result[] = array(
            'value' => $menuItem->getUrl(),
            'label' => $fullName . $menuItem->getTitle()
        );

        if ($menuItem->hasChildren()) {
            $this->getMenuArray($menuItem->getChildren(), $result);
        }
    }

    return $result;
}

Because we keep passing a reference of the initial array, we can add all the entries accordingly. There are two problems we need to fix, though.

First of all, the labels we get, will not be very meaningful. In the original PulseStorm Launcher sub menus were separated by a dash. Let’s replicate that:

const ITEMS_SEPARATOR = ' - ';

public function getMenuArray($menu, & $result = array(), $fullName = '')
{
    if (! empty($fullName)) {
        $fullName .= self::ITEMS_SEPARATOR;
    }

    foreach ($this->getMenuIterator($menu) as $menuItem) {
        /** @var $menuItem \Magento\Backend\Model\Menu\Item  */
        $result[] = array(
            'value' => $menuItem->getUrl(),
            'label' => $fullName . $menuItem->getTitle()
        );

        if ($menuItem->hasChildren()) {
            $this->getMenuArray($menuItem->getChildren(), $result, $fullName . $menuItem->getTitle());
        }
    }

    return $result;
}

The second problem is that some of the menu entries are meaningless in a launcher because they are merely textual; they don’t have an associated URL. We will skip those:

if ($menuItem->getUrl() !== '#') {
    // Only add meaningful entries
    $result[] = array(
        'value' => $menuItem->getUrl(),
        'label' => $fullName . $menuItem->getTitle()
    );
}

For those still spooked by the recursion part, here’s an example of a run:

  • Root items. Found “Products” menu. $fullName is empty. url equals “#”. Do not add to $result
  • “Products” menu has children. Take them to the next iteration with “Products” in $fullName.
  • “Products” items. Found “Inventory” menu. $fullName is not empty, add the dash. Append “Inventory” to “Products – ” and add it to $result.
  • “Inventory” menu has children… Rinse and repeat

Feed them to launcher.js

As you might have noticed, the array we built already has the entires with an associative value and label pair. This means we can directly convert them to JSON and set them to the launcher_items JavaScript variable:

public function getMenuJson()
{
    $menuArray = $this->getMenuArray($this->getMenuModel());
    return json_encode($menuArray);
}

And the accompanying update to launcher.phtml:

<script type="text/javascript">
var launcher_items = <?php echo $this->getMenuJson(); ?>;
</script>

Magento URL protection

It felt like we were done, right? However, if you look at the rendered source code of the Magento admin panel, you will notice that the URLs in the JavaScript array are invalid. This is because the URLs retrieved from the menu items still have placeholders for Magento its URL tokens.

The Menu block class solves this by replacing those placeholders in the afterToHtml handler which is called after the block is rendered but before its contents are put into the layout.

Because again, those methods are protected we will have to copy that logic over to our LauncherItems block class. Note that the \Magento\Backend\Model\UrlInterface should be added as a dependency in the constructor.

protected function _afterToHtml($html)
{
    $html = preg_replace_callback(
        '#' . \Magento\Backend\Model\UrlInterface::SECRET_KEY_PARAM_NAME . '/\$([^\/].*)/([^\/].*)/([^\$].*)\$#U',
        array($this, '_callbackSecretKey'),
        $html
    );

    return $html;
}

protected function _callbackSecretKey($match)
{
    return \Magento\Backend\Model\UrlInterface::SECRET_KEY_PARAM_NAME . '/' . $this->_url->getSecretKey(
        $match[1],
        $match[2],
        $match[3]
    );
}

 

The absolute final change we need to make is to add the JSON_UNESCAPED_SLASHES flag-parameter to json_encode in getMenuJson(). Otherwise, the RegEx used will break.

Refresh the admin panel for the last time and boot the launcher to see it at work.

Final words

As Magento 2 is coming, it’s a good idea to get your feet wet instead of waiting until the last hour. Hopefully this tutorial is a good motivation and/or introduction to get started with Magento 2 development.

The launcher we have developed is far from perfect and we therefore hope to welcome you in follow-up tutorials to expand its functionality and further explore Magento 2 development together.

The code used for this tutorial is available on Github. If you have anything to add or improvements to make, feel free to send a pull request.

Subscribe Newsletter

Subscribe to get latest Magento news

40% Off for 4 Months on Magento Hosting + 30 Free Migration