Thursday 5 January 2012

Magento for Developers: Part 4 - Magento Layouts, Blocks and Templatese


Developers new to Magento are often confused by the Layout and View system. This article will take a look at Magento's Layout/Block approach, and show you how it fits into Magento MVC worldview.
Unlike many popular MVC systems, Magento's Action Controller does not pass a data object to the view or set properties on the view object. Instead, the View component directly references system models to get the information it needs for display.
One consequence of this design decision is that the View has been separated into Blocks and Templates. Blocks are PHP objects, Templates are "raw" PHP files (with a .phtml extension) that contain a mix of HTML and PHP (where PHP is used as a templating language). Each Block is tied to a single Template file. Inside a phtml file, PHP's $this keyword will contain a reference to the Template's Block object.

A quick example

Take a look a the default product Template at the file at
app/design/frontend/base/default/template/catalog/product/list.phtml
You'll see the following PHP template code.
<?php $_productCollection=$this->getLoadedProductCollection() ?>    
    <?php 
if(!$_productCollection->count()): ?> <div class="note-msg">
        
<?php echo $this->__("There are no products matching the selection."?>    </div>
    
<?php else: ?>
The getLoadedProductCollection method can be found in the Template's Block,Mage_Catalog_Block_Product_List as shown:
File: app/code/core/Mage/Catalog/Block/Product/List.php
...
public function 
getLoadedProductCollection(){
    
return $this->_getProductCollection();}   ...
The block's _getProductCollection then instantiates models and reads their data, returning a result to the template.

Nesting Blocks

The real power of Blocks/Templates come with the getChildHtml method. This allows you to include the contents of a secondary Block/Template inside of a primary Block/Template.
Blocks calling Blocks calling Blocks is how the entire HTML layout for your page is created. Take a look at the one column layout Template.
File: app/design/frontend/base/default/template/page/one-column.phtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<
html xmlns="http://www.w3.org/1999/xhtml" xml:lang="?php echo $this->getLang() ?>" lang="<?php echo $this->getLang() ?>">
<
head><?php echo $this->getChildHtml('head'?></head>
<
body class="page-popup <?php echo $this->getBodyClass()?$this->getBodyClass():'' ?>">
    
<?php echo $this->getChildHtml('content'?>
    <?php 
echo $this->getChildHtml('before_body_end'?>
    <?php 
echo $this->getAbsoluteFooter() ?></body>
The template itself is only 11 lines long. However, each call to $this->getChildHtml(...) will include and render another Block. These Blocks will, in turn, use getChildHtml to render other Blocks. It's Blocks all the way down.

The Layout

So, Blocks and Templates are all well and good, but you're probably wondering
  1. How do I tell Magento which Blocks I want to use on a page?
  2. How do I tell Magento which Block I should start rendering with?
  3. How do I specify a particular Block in getChildHtml(...)? Those argument strings don't look like Block names to me.
This is where the Layout Object enters the picture. The Layout Object is an XML object that will define which Blocks are included on a page, and which Block(s) should kick off the rendering process.
Last time we were echoing content directly from out Action Methods. This time let's create a simple HTML template for our Hello World module.
First, create a file at
app/design/frontend/base/default/layout/local.xml
with the following contents
<layout version="0.1.0">
    <default>
        <
reference name="root">
            <
block type="page/html" name="root" output="toHtml" template="../../../../../code/local/Magentotutorial/Helloworld/simple_page.phtml" />
        </
reference>
    </default>
</
layout>
Then, create a file at
app/code/local/Magentotutorial/Helloworld/simple_page.phtml
with the following contents
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
>
<
html xmlns="http://www.w3.org/1999/xhtml">
<
head>
    <
title>Untitled</title>
    <
meta name="generator" content="BBEdit 9.2" />
    <
style type="text/css">
        
body {
            background
-color:#f00;
        
}
    
</style>
</
head>
<
body>

</
body>
</
html>
Finally, each Action Controller is responsible for kicking off the layout process. We'll need to add two method calls to the Action Method.
public function indexAction() {
    
//remove our previous echo
    //echo 'Hello Index!';
    
$this->loadLayout();
    
$this->renderLayout();}
Clear your Magento cache and reload your Hello World controller page. You should now see a website with a bright red background and an HTML source that matches what's in simple_page.phtml.

What's Going On

So, that's a lot of voodoo and cryptic incantations. Let's take a look at what's going on.
First, you'll want to install the Layoutviewer module. This is a module similar to the Configviewer module you built in the Hello World article that will let us peek at some of Magento's internals.
Once you've installed the module (similar to how you setup the Configviewer module), go to the following URL
http://example.com/helloworld/index/index?showLayout=page
This is the layout xml for your page/request. It's made up of <block />, <reference /> and <remove /> tags. When you call the loadLayout method of your Action Controller, Magento will
  1. Generate this Layout XML
  2. Instantiate a Block class for each <block /> and <reference /> tag, looking up the class using the tag's name attribute as a global config path and store in the internal _blocks array of the layout object.
  3. If the <block /> tag contains an output attribute, its value is added to the internal _output array of the layout object.
Then, when you call the renderLayout method in your Action Controller, Magento will iterate over all the Blocks in the _blocks array, using the value of the output attribute as a callback method. This is almost always toHtml, and means the starting point for output will be that Block's Template.
The following sections will cover how Blocks are instantiated, how this layout file is generated, and finishes up with kicking off the output process.

Block Instantiation

So, within a Layout XML file, a <block /> or <reference /> has a "type" that's actually a Grouped Class Name URI
<block type="page/html" ...
<
block type="page/template_links"
The URI references a location in the (say it with me) global config. The first portion of the URI (in the above examplespage) will be used to query the global config to find the page class name. The second portion of the URI (in the two examples above, html and template_links) will be appended to the base class name to create the class Magento should instantiate.
We'll go through page/html as an example. First, Magento looks for the global config node at
/global/blocks/page
and finds
<page>
    <class>
        
Mage_Page_Block
    
</class>
</
page>
This gives us our base class name Mage_Page_Block. Then, the second part of the URI (html) is appended to the class name to give us our final Block class name Mage_Page_Block_Html. This is the class that will be instantiated.
Remember, Blocks are one of the Grouped Class Names in Magento, all which share a similar instantiation method.

The Difference Between <block /> and <reference />

We mentioned that both <blocks /> and <refernces /> will instantiate Block classes, and you're probably wondering what the difference is.
<reference />'s are used to replace existing Blocks in a layout file. For example, consider the following layout snippet.
<block type="page/html" name="root" output="toHtml" template="page/2columns-left.phtml">
    <!-- ... 
sub blocks ... -->
</
block>
<!-- ... -->
<
reference name="root">
    <
block type="page/someothertype" name="root" template="path/to/some/other/template" />
    <!-- ... 
sub blocks ... -->
    </
block>
</
reference>
Magento initially creates a page/html Block named root. Then, when it later encounters the reference with the same name (root), it will replace the original root <block /> with the <block /> enclosed in the <reference />.
This is what we've done in our local.xml file from above.
<layout version="0.1.0">
    <default>
        <
reference name="root">
            <
block type="page/html" name="root" output="toHtml" template="../../../../../code/local/Magentotutorial/Helloworld/simple_page.phtml" />
        </
reference>
    </default>
</
layout>
The Block named root has been replaced with our Block, which points at a different phtml Template file.

How Layout Files are Generated

So, we have a slightly better understanding of what's going on with the Layout XML, but where is this XML file coming from? To answer that question, we need to introduce two new concepts; Handles and the Package Layout.

Handles

Each page request in Magento will generate several unique Handles. The Layoutview module can show you these Handles by using a URL something like
http://example.com/helloworld/index/index?showLayout=handles
You should see a list similar to the following (depending on your configuration)
  1. default
  2. STORE_bare_us
  3. THEME_frontend_default_default
  4. helloworld_index_index
  5. customer_logged_out
Each of these is a Handle. Handles are set in a variety of places within the Magento system. The two we want to pay attention to are default and helloworld_index_index. The default Handle is present in everyrequest into the Magento system. The helloworld_index_index Handle is created by combining the frontName (helloworld), Action Controller name (index), and Action Controller Action Method (index) into a single string. This means each possible method on an Action Controller has a Handle associated with it.
Remember that "index" is the Magento default for both Action Controllers and Action Methods, so the following request
http://example.com/helloworld/?showLayout=handles
Will also produce a Handle named helloworld_index_index

Package Layout

You can think of the Package Layout similar to the global config. It's a large XML file that contains every possible layout configuration for a particular Magento install. Let's take a look at it using the Layoutview module
http://example.com/helloworld/index/index?showLayout=package
This may take a while to load. If your browser is choking on the XML rendering, try the text format
http://example.com/helloworld/index/index?showLayout=package&showLayoutFormat=text
You should see a very large XML file. This is the Package Layout. This XML file is created by combining the contents of all the XML layout files for the current theme (or package). For the default install, this is at
app/design/frontend/base/default/layout/
Behind the scenes there's an <updates /> section of the global config that contains nodes with all the file names to load. Once the files listed in the config have been combined, Magento will merge in one last xml file, local.xml. This is the file where you're able to add your customizations to your Magento install.

Combining Handles and The Package Layout

So, if you look at the Package Layout, you'll see some familiar tags such as <block /> and <reference />, but they're all surrounded by tags that look like
<default />
<
catalogsearch_advanced_index />etc...
These are all Handle tags. The Layout for an individual request is generated by grabbing all the sections of the Package Layout that match any Handles for the request. So, in our example above, our layout is being generated by grabbing tags from the following sections
<default />
<
STORE_bare_us />
<
THEME_frontend_default_default />
<
helloworld_index_index />
<
customer_logged_out />
There's one additional tag you'll need to be aware of in the Package Layout. The <update /> tag allows you to include another Handle's tags. For example
<customer_account_index>
    <!-- ... -->
    <
update handle="customer_account"/>
    <!-- ... -->
</
customer_account_index>
Is saying that requests with a customer_account_index Handle should include <reference />s and <blocks />s from the <customer_account /> Handle.

Applying What We've Learned

OK, that's a lot of theory. Lets get back to what we did earlier. Knowing what we know now, adding
<layout version="0.1.0">
    <default>
        <
reference name="root">
            <
block type="page/html" name="root" output="toHtml" template="../../../../../code/local/Magentotutorial/Helloworld/simple_page.phtml" />
        </
reference>
    </default>
</
layout>
to local.xml means we've overridden the "root" tag. with a different Block. By placing this in the <default /> Handle we've ensured that this override will happen for every page request in the system. That's probably not what we want.
If you go to any other page in your Magento site, you'll notice they're either blank white, or have the same red background that your hello world page does. Let's change your local.xml file so it only applies to the hello world page. We'll do this by changing default to use the full action name handle (helloworld_index_index).
<layout version="0.1.0">
    <
helloworld_index_index>
        <
reference name="root">
            <
block type="page/html" name="root" output="toHtml" template="../../../../../code/local/Magentotutorial/Helloworld/simple_page.phtml" />
        </
reference>
    </
helloworld_index_index>
</
layout>
Clear your Magento cache, and the rest of your pages should be restored.
Right now this only applies to our index Action Method. Let's add it to the goodbye Action Method as well. In your Action Controller, modify the goodbye action so it looks like
public function goodbyeAction() {
    $this
->loadLayout();
    
$this->renderLayout();          }
If you load up the following URL, you'll notice you're still getting the default Magento layout.
http://example.com/helloworld/index/goodbye
We need to add a Handle for the full action name (helloworldindexgoodbye) to our local.xml file. Rather than specify a new <reference />, lets use the update tag to match the helloworld_index_index Handle.
<layout version="0.1.0">
    <!-- ... -->
    <
helloworld_index_goodbye>
        <
update handle="helloworld_index_index" />
    </
helloworld_index_goodbye>
</
layout>
Loading the following pages (after clearing your Magento cache) should now produce identical results.
http://example.com/helloworld/index/index
http://example.com/helloworld/index/goodbye

Starting Output and getChildHtml

In a standard configuration, output starts on the Block named root (because it has an output attribute). We've overridden root's Template with our own
template="../../../../../code/local/Magentotutorial/Helloworld/simple_page.phtml"
Templates are referenced from the root folder of the current theme. In this case, that's
app/design/frontend/base/default
so we need to climb five directories (../../../../../) then drill down to our custom page. Most Magento Templates are stored in
app/design/frontend/base/default/templates

Adding Content Blocks

A simple red page is pretty boring. Let's add some content to this page. Change your<helloworld_index_index /> Handle in local.xml so it looks like the following
<helloworld_index_index>
    <
reference name="root">
        <
block type="page/html" name="root" template="../../../../../code/local/Magentotutorial/Helloworld/simple_page.phtml">
            <
block type="customer/form_register" name="customer_form_register" template="customer/form/register.phtml"/>
        </
block>
    </
reference>
</
helloworld_index_index>
We're adding a new Block nested within our root. This is a Block that's distributed with Magento, and will display a customer registration form. By nesting this Block within our root Block, we've made it available to be pulled into oursimple_page.html Template. Next, we'll use the Block's getChildHtml method in our simple_page.phtml file. Edit simple_page.html so it looks like this
<body>
    
<?php echo $this->getChildHtml('customer_form_register'); ?></body>
Clear your Magento cache and reload the page and you should see the customer registration form on your red background. Magento also has a Block named top.links. Let's try including that. Change your simple_page.html file so it reads
<body>
    <
h1>Links</h1>
    
<?php echo $this->getChildHtml('top.links'); <?php echo '?';?>>
</
body>
When you reload the page, you'll notice that your <h1>Links</h1> title is rendering, but nothing is rendering for top.links. That's because we didn't add it to local.xml. The getChildHtml method can only include Blocks that are specified as sub-Blocks in the Layout. This allows Magento to only instantiate the Blocks it needs, and also allows you to set difference Templates for Blocks based on context.
Let's add the top.links Block to our local.xml
<helloworld_index_index>
    <
reference name="root">
        <
block type="page/html" name="root" template="../../../../../code/local/Magentotutorial/Helloworld/simple_page.phtml">
            <
block type="page/template_links" name="top.links"/>
            <
block type="customer/form_register" name="customer_form_register" template="customer/form/register.phtml"/>
        </
block>
    </
reference>
</
helloworld_index_index>
Clear your cache and reload the page. You should now see the top.links module.

Wrapup

That covers Layout fundamentals. If you found it somewhat daunting, don't worry, you'll rarely need to work with layouts on such a fundamental level. Magento provides a number of pre-built layouts which can be modified and skinned to meet the needs of your store. Understanding how the entire Layout system works can be a great help when you're trouble shooting Layout issues, or adding new functionality to an existing Magento system.

No comments:

Post a Comment