- 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
As promised, today we’re going to show you how to add javascript and CSS files to your Magento module without needing to worry about how their paths are rendered or how Magento serves them. While you may find this article useful on its own, we’re going to assume you’ve worked through the previous two.
Before we get to the main course, we’re going to provide an engineering critique of Magento 2’s XML based layout rendering language. This language is similar to the XML based language in Magento 1, but has some differences that might trip up an experienced Magento 1 developer.
The next few sections are optional, and not recommended unless you’re interested in the implementation details of Magento 2. You can skip ahead to the Adding CSS and Javascript to a Page section if all you’re interested in is getting your javascript and CSS files included on a page.
Magento 2’s Domain Specific Language for Rendering HTML
With Magento 2, the core team has added a number of features to their XML based layout language. They’ve also changed some of the language’s semantics. For example, in Magento 1, each node under the root node of a layout update XML file was always a layout handle. For example, in the Magento 1 Layout Update XML Node below, the handle is catalog_category_view
.
<catalog_category_view>
<block ...>
<!-- ... -->
</block>
</catalog_category_view>
Without getting too deeply into it, handles control which layout nodes are applied to a page during which request. Magento 2 still has handles, but they no longer appear in layout files. Instead, the handle is the file name. For example, the main catalog_category_view
layout handle XML file is at
./vendor/magento/module-catalog/view/frontend/layout/catalog_category_view.xml
Other modules that listen for the catalog_category_view
handle include
./vendor/magento/module-checkout/view/frontend/layout/catalog_category_view.xml
./vendor/magento/module-directory/view/frontend/layout/catalog_category_view.xml
./vendor/magento/module-google-optimizer/view/frontend/layout/catalog_category_view.xml
./vendor/magento/module-msrp/view/frontend/layout/catalog_category_view.xml
./vendor/magento/module-paypal/view/frontend/layout/catalog_category_view.xml
./vendor/magento/module-swatches/view/frontend/layout/catalog_category_view.xml
./vendor/magento/module-wishlist/view/frontend/layout/catalog_category_view.xml
In all the examples, the file name is the handle name. While the mechanism has changed, handles still serve the same purpose in Magento 2. On every Magento 2 HTML page render, certain handles “fire”, similar to events. The handles that fire control which layout files are loaded (in Magento 1 they controlled which XML nodes were read from all the files), and then Magento processes the combined XML tree to know which blocks it should add to a page.
Put another way, in Magento 1 you would
- Configure your module to load a layout update XML file (
namespace_module.xml
,catalog.xml
etc.) - In the layout update XML file you’d add a node for your handle (
catalog_category_view
) - Under the
catalog_category_view
node you’d add your blocks
In Magento 2, you
- Add a layout handle XML file (
catalog_category_view.xml
) - Under the root node of your layout handle XML file, add your blocks
We covered this change briefly in our Introduction to Magento 2 — No More MVC article. However, we glossed over a number of substantial changes to Magento’s layout language that we’ll need to touch briefly on before we continue.
New Nodes
In our Introduction to Magento 2 — No More MVC article, we created the following node in the layout handle XML file.
<!-- File: app/code/Pulsestorm/HelloWorldMVVM/view/frontend/layout/hello_mvvm_hello_world.xml -->
<referenceBlock name="content">
<block
template="content.phtml"
class="Pulsestorm\HelloWorldMVVM\Block\Main"
name="pulsestorm_helloworld_mvvm"/>
</referenceBlock>
As a reminder, this XML is roughly equivalent to the following pseudo code.
//pseudo code -- does not work
$our_view_block =
$layout->createNewBlockWithClass('Pulsestorm\HelloWorldMVVM\Block\Main')
$our_view_block->setName('pulsestorm_helloworld_mvvm');
$out_view_block->setTemplate('content.phtml');
$layout->addBlockToContentContainer($our_view_block);
The <block/>
tag from Magento 1 remains relatively unchanged. It means Create a block object. The main difference is the type
attribute has been replaced with a class
attribute. Since Magento did away with class aliases (core/template
, namespace_module/my_block
, etc.) it made sense to do away with the type
attribute, and more accurately label it as class
.
The first small change above is the referenceBlock
node. Magento 1 had the concept of a block reference. However, the node was named <reference/>
. In Magento 1, the above might look like
<reference name="content">
<block
template="content.phtml"
class="Pulsestorm\HelloWorldMVVM\Block\Main"
name="pulsestorm_helloworld_mvvm"/>
</block>
The referenceBlock
node makes things more explicit, and is another welcome change. This might seem superficial, until you realize that Magento 2’s layout language controls more than blocks. The layout language also controls something called containers, and has a corresponding referenceContainer
block. You can see an example of the referenceContainer
block here
<!-- vendor/magento/module-checkout/view/frontend/layout/checkout_shipping_price_renderer.xml -->
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd">
<referenceContainer name="root">
<block class="Magento\Checkout\Block\Shipping\Price" name="checkout.shipping.price" as="shipping.price" template="shipping/price.phtml"/>
</referenceContainer>
</layout>
In Magento 2, a container is a special sort of block that only contains other blocks. Containers are conceptually similar to the text/list
blocks in Magento 1, although their implementation is very different.
The concept of containers is a good one, but it’s here that the implementation starts to get a little wobbly. Magento’s layout language is a little loosey–goosey with the difference between a container and a block. For example, the above XML?
<referenceContainer name="root">
This could also be written as
<referenceBlock name="root">
That is — even though root
is a container, referenceBlock
will still return a reference to it, and allow you to add blocks to it. For a change meant to make things more explicit and clear, it’s a little strange that the layout language would let something like that happen.
White Lies
Remember this XML from the introduction article?
<!-- File: app/code/Pulsestorm/HelloWorldMVVM/view/frontend/layout/hello_mvvm_hello_world.xml -->
<referenceBlock name="content">
<block
template="content.phtml"
class="Pulsestorm\HelloWorldMVVM\Block\Main"
name="pulsestorm_helloworld_mvvm"/>
</referenceBlock>
Well, it turns out that the content
block is actually a container. The above should have been written as
<!-- File: app/code/Pulsestorm/HelloWorldMVVM/view/frontend/layout/hello_mvvm_hello_world.xml -->
<referenceContainer name="content">
<block
template="content.phtml"
class="Pulsestorm\HelloWorldMVVM\Block\Main"
name="pulsestorm_helloworld_mvvm"/>
</referenceContainer>
We used referenceBlock
in our introduction tutorial because we weren’t ready to discuss containers and other changes to the layout system. While this was useful for a transitional tutorial, generally speaking this is the sort of looseness that can make a domain specific language seem extra confusing.
Without getting too deeply into the details, you can tell if a “block” is a container or a regular block by how the original programmer created it. If you see a <block/>
tag
<block name="foo" />
then the named block (“foo” above) is a regular block. If you see a <container/>
tag
<container name="foo"/>
then the named entity is a container. If you’re curious, Magento’s core code adds the content
container in the following file
<!-- vendor/magento/module-theme/view/frontend/layout/default.xml -->
<container name="content" label="Main Content Area"/>
Also, notice the default.xml
file name? That’s equivalent to Magento 1’s <default/>
handle node.
Context Sensitive Nodes
In Magento 1, the layout language was a system designed to render arbitrary HTML via a nested collection of block objects. The layout system itself didn’t care which part of an HTML document it was rendering. It just rendered blocks. Specific blocks, like page/html_head
, could introduce that context, but it happened at the block level. The layout system itself was unaware that it was rendering the <head/>
portion of a document.
In Magento 2, the core team attempted to change this, and add that context in at the language level. They added two new top level tags named <body>
and <head>
to the vocabulary of the language. While it was an interesting experiment, the implementation feels half done, and further complicates an already complicated layout system. Consider the following
<!-- File: vendor/magento/module-backend/view/adminhtml/layout/default.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="admin-1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<title>Magento Admin</title>
<meta name="viewport" content="width=1024, initial-scale=1"/>
<link src="requirejs/require.js"/>
<css src="extjs/resources/css/ext-all.css"/>
<css src="extjs/resources/css/ytheme-magento.css"/>
</head>
<body>
<attribute name="id" value="html-body"/>
<block name="require.js" class="Magento\Backend\Block\Page\RequireJs" template="Magento_Backend::page/js/require_js.phtml"/>
<referenceContainer name="global.notices">
<block class="Magento\Backend\Block\Page\Notices" name="global_notices" as="global_notices" template="page/notices.phtml"/>
</referenceContainer>
<!-- ... -->
</body>
</page>
Here you can see an example of a core module layout handle XML file that uses the new head
and body
sections. The first bit of confusion this introduces is top level tags under the root tag now mean different things. In some files, these top level tags will be context tags like <head/>
and <body/>
above. In other files, the top level tags will be actual commands/directives (referenceBlock
, container
, etc) for the layout engine
<!-- File: vendor/magento/module-bundle/view/base/layout/catalog_product_prices.xml -->
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd">
<referenceBlock name="render.product.prices">
<!-- ... -->
</layout>
If you enjoy implementing domain specific languages, this may seem like a minor thing. However, the intent of a domain specific language is to simplify and constrain the options for developers and programmers unfamiliar with the entire system. The lack of consistency here will make these files harder for designers and front end developers to understand.
The next bit of confusion is in what that context change means. Nodes placed inside the <body/>
tag
<!-- File: vendor/magento/module-backend/view/adminhtml/layout/default.xml -->
<body>
<attribute name="id" value="html-body"/>
<block name="require.js" class="Magento\Backend\Block\Page\RequireJs" template="Magento_Backend::page/js/require_js.phtml"/>
<referenceContainer name="global.notices">
<block class="Magento\Backend\Block\Page\Notices" name="global_notices" as="global_notices" template="page/notices.phtml"/>
</referenceContainer>
<!-- ... -->
</body>
behave very similar to plain old layout XML nodes. You’re still getting references to existing blocks and containers, and adding new blocks to them for rendering. The only difference is the <attribute/>
tag you see above. With this you can change the ID element of the underlying <body/>
tag.
When you shift into <head/>
context, you’re in a difference world.
<!-- File: vendor/magento/module-backend/view/adminhtml/layout/default.xml -->
<head>
<title>Magento Admin</title>
<meta name="viewport" content="width=1024, initial-scale=1"/>
<link src="requirejs/require.js"/>
<css src="extjs/resources/css/ext-all.css"/>
<css src="extjs/resources/css/ytheme-magento.css"/>
</head>
Here, you’ve completely lost the ability to modify the layout with commands like referenceBlock
, etc. Instead, you have a narrow set of tags (<attribute/>
, <css/>
, <link/>
, <meta/>
, <remove/>
, <script/>
, <title/>
) for doing things specifically in the <head/>
of a document.
The other bit of cognitive dissonance a Magento 1 developer will feel here is the <head/>
section of the HTML page is no longer rendered like a normal block. If you take a look at the root phtml template you can see a Magento HTML page is no longer a series of nested blocks.
<!-- File: vendor/magento/module-theme/view/base/templates/root.phtml -->
<!doctype html>
<html <?php /* @escapeNotVerified */ echo $htmlAttributes ?>>
<head <?php /* @escapeNotVerified */ echo $headAttributes ?>>
<?php /* @escapeNotVerified */ echo $requireJs ?> <?php /* @escapeNotVerified */ echo $headContent ?> <?php /* @escapeNotVerified */ echo $headAdditional ?> </head>
<body data-container="body" data-mage-init='{"loaderAjax": {}, "loader": { "icon": "<?php /* @escapeNotVerified */ echo $loaderIcon; ?>"}}' <?php /* @escapeNotVerified */ echo $bodyAttributes ?>>
<?php /* @escapeNotVerified */ echo $layoutContent ?> </body>
</html>
In Magento 2, an HTML page is a phtml
template populated by simple variables. These simple variables are populated by different means in the render method of the Magento\Framework\View\Result\Page
object. Magento creates the <body/>
tag of the page by echoing out the $layoutContent
variable. Magento gets the string for $layoutContent
by doing the traditional kickoff of rendering a series of nested blocks.
#File: vendor//magento/framework/View/Result/Page.php
$output = $this->getLayout()->getOutput();
$this->assign('layoutContent', $output);
//...
<?php /* @escapeNotVerified */ echo $layoutContent ?>
Magento renders the <head/>
section of an HTML page by echoing several variables.
<head <?php /* @escapeNotVerified */ echo $headAttributes ?>>
<?php /* @escapeNotVerified */ echo $requireJs ?> <?php /* @escapeNotVerified */ echo $headContent ?> <?php /* @escapeNotVerified */ echo $headAdditional ?> </head>
How Magento populates of contents of these variable is beyond the scope of this article. The main change you’ll want to be aware of is that <head/>
is no longer simply controlled by standard layout blocks.
Summary of Magento 2 Layout Changes
Magento 1’s layout system, while cryptic, was ultimately understandable by a single developer. Its why I wrote No Frills Magento Layout — the system wasn’t well documented, but once explained developers could understand and reason about it from top to bottom. It had a complex looking surface, but a simple elegant implementation.
As you can see from the above critique, Magento 2 has taken an already cryptic system, and added layers of complexity on top of it. These top levels of complexity are equally complex under the hood. Without getting into the details of it, Magento takes the layout handle XML files for a single request, merges them into a a single document, processes that document to transform it into a Magento 1 style page layout document, and then processes that document is way that similar, but not identical, to Magento 1.
Unlike Magento 1’s layout system, which an average developer could ultimately translate in their head into PHP code and reason about, the new rendering is too complex for most human beings to keep in their head at once. The new system is less understandable to the average developer. Perhaps this was necessary to implement the RequireJS and Less CSS systems the core team wanted to, but from the outside looking in it seems like a classic case of what people complain about when they complain about architect driven development.
Adding CSS and Javascript to a Page
Layout system critiqued, let’s get back to the practical business of adding a front end file to our Magento module. Before we begin, per pervious articles in this series, the following assumes
- You’re working with developer mode enabled (
SetEnv MAGE_MODE developer
in your apache config) - That you’ve disabled the full page caching in
System -> Cache Managment
To start, we need to create a new Magento module. We’re going to use the Magento code generating tool pestle
for this, but if you want to create your own module manually you can follow the instructions in our Introduction to Magento 2 — No More MVC article.
To create the new module using pestle
, run the following commands
$ pestle.phar generate_module Pulsestorm JavascriptCssExample 0.0.1
$ pestle.phar generate_route Pulsestorm_JavascriptCssExample frontend pulsestorm_javascriptcssexample
$ pestle.phar generate_view Pulsestorm_JavascriptCssExample frontend pulsestorm_javascriptcssexample_index_index Main content.phtml
Then enable your new module
$ php bin/magento module:enable Pulsestorm_JavascriptCssExample
and let the module setup system know about your module.
$ php bin/magento setup:upgrade
After running the above, you should be able to load the pulsestorm_javascriptcssexample
frontname in your system.
http://magento.example.com/pulsestorm_javascriptcssexample
With the above completed, let’s get started!
The Layout Head Section
One of the new features Magento 2 introduces is context aware layout update XML files. By context aware we mean that end-programmer-users can add commands/directives to their layout XML files that only effect a particular section of the document. In plain english — layout update XML files now have a <head/>
section where you can add head specific information about a file.
Sample code is often worth 1,000 words. Open up your module’s layout handle XML file
<!-- File: app/code/Pulsestorm/JavascriptCssExample/view/frontend/layout/pulsestorm_javascriptcssexample_index_index.xml -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="../../../../../../../lib/internal/Magento/Framework/View/Layout/etc/page_configuration.xsd">
<referenceBlock name="content">
<block template="content.phtml" class="Pulsestorm\JavascriptCssExample\Block\Main" name="pulsestorm_javascriptcssexample_block_main" />
</referenceBlock>
</page>
and add the following <head/>
node
<!-- File: app/code/Pulsestorm/JavascriptCssExample/view/frontend/layout/ -->pulsestorm_javascriptcssexample_index_index.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="../../../../../../../lib/internal/Magento/Framework/View/Layout/etc/page_configuration.xsd">
<head>
<css src="Pulsestorm_JavascriptCssExample::test.css"/>
<link src="Pulsestorm_JavascriptCssExample::test.js"/>
</head>
<referenceBlock name="content">
<block template="content.phtml" class="Pulsestorm\JavascriptCssExample\Block\Main" name="pulsestorm_javascriptcssexample_block_main" />
</referenceBlock>
</page>
After making the above changes, clear your cache,
$ php bin/magento cache:clean
and then reload the Magento page
http://magento.example.com/pulsestorm_javascriptcssexample
If you view the source to this page, you should see the following HTML
<link rel="stylesheet" type="text/css" media="all" href="http://magento-2-with-keys.dev/static/frontend/Magento/blank/en_US/Pulsestorm_JavascriptCssExample/test.css" />
<!-- ... -->
<script type="text/javascript" src="http://magento-2-with-keys.dev/static/frontend/Magento/blank/en_US/Pulsestorm_JavascriptCssExample/test.js"></script>
That is, Magento has automatically created the http
(or https
) paths we’ve been generating manually so far.
http://magento-2-with-keys.dev/static/frontend/Magento/blank/en_US/Pulsestorm_JavascriptCssExample/test.css
http://magento-2-with-keys.dev/static/frontend/Magento/blank/en_US/Pulsestorm_JavascriptCssExample/test.js
When you use a string identifier like this
Pulsestorm_JavascriptCssExample::test.css
you’re telling Magento
use the
test.css
file found in thePulsestorm_JavascriptCssExample
module.
Without these Vendor_Module::...
identifiers Magento would try loading these files from the theme hierarchy.
With the URLs generated, if you add corresponding files to your module at
#File: app/code/Pulsestorm/JavascriptCssExample/view/frontend/web/test.js
alert("hello");
#File: app/code/Pulsestorm/JavascriptCssExample/view/frontend/web/test.css
body{
background-color:#f00;
}
and reload the page, you’ll see that Magento has loaded them correctly into the system.
Adding Files Via PHP
In addition to using Magento 2’s layout XML system to automatically add front end asset URLs to your project, you can also create these URLs via PHP using a Magento\Framework\View\Asset\Repository
object. We’ll show you how to do this below, as well as how to add arbitrary HTML to the <head/>
of a Magento HTML page.
Starting with the later item, add the following node to our layout handle XML file
<!-- File: app/code/Pulsestorm/JavascriptCssExample/view/frontend/layout/ pulsestorm_javascriptcssexample_index_index.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="../../../../../../../lib/internal/Magento/Framework/View/Layout/etc/page_configuration.xsd">
<!-- ... -->
<referenceBlock name="head.additional">
<block template="head.phtml"
class="Pulsestorm\JavascriptCssExample\Block\Head"
name="pulsestorm_javascriptcssexample_block_head" />
</referenceBlock>
<!-- ... -->
</page>
The above code
- Gets a reference to the
head.additional
block - Creates a new
Pulsestorm\JavascriptCssExample\Block\Head
block namedpulsestorm_javascriptcssexample_block_head
that uses thehead.phtml
template. - Adds that new block to the
head.additional
block using the reference from #1
The head.additional
block is a special block. Any block added to head.additional
will automatically be output into the <head/>
area of a Magento page. If you read our critique above, this is another bit of confusion added by the <head/>
context. Even though our ultimate goal is to add something to <head/>
, we need to operate inside the layout handle XML file’s <body/>
tag.
Regardless, once we’ve got the layout XML in place, we’ll want to create our new Head
block class
#File: app/code/Pulsestorm/JavascriptCssExample/Block/Head.php
<?php
namespace Pulsestorm\JavascriptCssExample\Block;
class Head extends \Magento\Framework\View\Element\Template
{
}
As well as a template
#File: app/code/Pulsestorm/JavascriptCssExample/view/frontend/templates/head.phtml
<!-- Hello There -->
With the above in place, clear your Magento cache and reload your page. You should see the <!-- Hello There -->
comment in your page’s <head/>
node.
With a new template rendered in <head/>
, we’re ready to render an asset URL using the asset repository.
The Asset Repository
The Magento\Framework\View\Asset\Repository
object will allow us to create asset objects. Asset objects can convert a file identifier like foo/test.js
or Pulsestorm_JavascriptCssExample::test.js
into a full URL.
Like any object in Magento 2, when we want an instance of an object we don’t directly instantiate it — we inject it in another object’s constructor. Change your Head.php
file so it matches the following
#File: app/code/Pulsestorm/JavascriptCssExample/Block/Head.php
<?php
namespace Pulsestorm\JavascriptCssExample\Block;
class Head extends \Magento\Framework\View\Element\Template
{
public $assetRepository;
public function __construct(
\Magento\Framework\View\Element\Template\Context $context,
array $data = [],
\Magento\Framework\View\Asset\Repository $assetRepository
)
{
$this->assetRepository = $assetRepository;
return parent::__construct($context, $data);
}
}
What we’ve done above is use Magento 2’s automatic constructor dependency injection to create a \Magento\Framework\View\Asset\Repository
object, and assign it to the assetRepository
property of our block object. The other parameters in __construct
and the call to parent::__construct
are there for compatibility with the base template block class. Also, notice we made assetRepository
a public property. This means we’ll be able to access it in our phtml
template.
Edit your head.phtml
file so it matches the following.
#File: app/code/Pulsestorm/JavascriptCssExample/view/frontend/templates/head.phtml
@highlightsyntax@php
<?php
$asset_repository = $this->assetRepository;
$asset = $asset_repository->createAsset('Pulsestorm_JavascriptCssExample::test.js');
$url = $asset->getUrl();
?>
<!-- Hello There -->
<script src="<?php echo $url; ?>"></script>
With the above in place, clear your cache, delete the files in var/generate/*
(because you changed an automatic constructor dependency injection constructor), and reload the page. If you view the raw HTML source, you should see a new <script/>
tag rendered with a full asset URL.
What we’ve done above is use the createAsset
method of the asset repository object to create an asset object. Then, we use the getUrl
method of the asset object to fetch the HTTP
url of the asset. All we need to know is the file identifier — Magento handles the grunt work of pulling together the correct URL path parameters.
Incorrect dependency in class
Update: Readers have reported that, in more modern versions of Magento 2, they end up getting the following error
Incorrect dependency in class
Pulsestorm\JavascriptCssExample\Block\Head
in app/code/Pulsestorm/JavascriptCssExample/Block/Head.php , \Magento\Framework\View\Asset\Repository already exists in context object
when running setup:di:compile
. You can fix this by
- Removing
Magento\Framework\View\Asset\Repository
injection from the constructor - Replace
$asset_repository = $this->assetRepository;
with$this->getAssetRepository()
- If you’re using a version of Magento that doesn’t set an asset repository object on blocks, try
$asset_repository = $this->_assetRepo;
instead.
The reason this error occurs is the dependency injection compiler checks to make sure objects don’t have “double injections”. The Magento\Framework\View\Asset\Repository
class is already injected in a base template class, and therefore our injection of it is redundant.
We regret the error.
Wrap Up
Today, after a long winded critique of Magento 2’s layout language, we demonstrated how to use that language to add front end CSS and Javascript assets to a Magento page. We also investigated directly using the underlying PHP asset repository that makes this possible.
In the past few articles, we’ve been entirely focused on getting “raw” front end asset files into our system. Next time we’ll start investigating how Magento has integrated with the new higher level front end abstractions like RequireJS and Less CSS.