Magento Create Block

This article will look at how Magento generates and creates its blocks from reading the layout configuration within its layout files.

If you haven’t already seen how Magento gets its layout config, it is recommended to read the Magento Load Layout article.

The generateBlocks() method is a good starting place to look at within the Mage_Core_Model_Layout class.

public function generateBlocks($parent=null) 
{
    if (empty($parent)) {
        $parent = $this->getNode();
    }
    foreach ($parent as $node) {
        $attributes = $node->attributes();
        if ((bool)$attributes->ignore) {
            continue;
        }
        switch ($node->getName()) {
            case 'block':
                $this->_generateBlock($node, $parent);
                $this->generateBlocks($node);
                break;

            case 'reference':
                $this->generateBlocks($node);
                break;

            case 'action':
                $this->_generateAction($node, $parent);
                break;
        }
    }
}

Here we can see a foreach loop that loops around our layout configuration and generates the blocks or actions accordingly.

The first node that gets checked is the formkey block within the core.xml layout file.

<?xml version="1.0"?>
<layout version="0.1.0">
    <default>
        <block name="formkey" type="core/template" template="core/formkey.phtml" />
    </default>
</layout>

So this node enters the switch statement with a node name of block and therefore proceeds to run the _generateBlock() method. As well as this, the generateBlocks() method is run again that checks if there are any children nodes within the current node.

The _generateBlock() method consists of the following code.

protected function _generateBlock($node, $parent)
{
    if (!empty($node['class'])) {
        $className = (string)$node['class'];
    } else {
        $className = (string)$node['type'];
    }

    $blockName = (string)$node['name'];
    $_profilerKey = 'BLOCK: '.$blockName;
    Varien_Profiler::start($_profilerKey);

    $block = $this->addBlock($className, $blockName);
    if (!$block) {
        return $this;
    }

    if (!empty($node['parent'])) {
        $parentBlock = $this->getBlock((string)$node['parent']);
    } else {
        $parentName = $parent->getBlockName();
        if (!empty($parentName)) {
            $parentBlock = $this->getBlock($parentName);
        }
    }
    if (!empty($parentBlock)) {
        $alias = isset($node['as']) ? (string)$node['as'] : '';
        if (isset($node['before'])) {
            $sibling = (string)$node['before'];
            if ('-'===$sibling) {
                $sibling = '';
            }
            $parentBlock->insert($block, $sibling, false, $alias);
        } elseif (isset($node['after'])) {
            $sibling = (string)$node['after'];
            if ('-'===$sibling) {
                $sibling = '';
            }
            $parentBlock->insert($block, $sibling, true, $alias);
        } else {
            $parentBlock->append($block, $alias);
        }
    }
    if (!empty($node['template'])) {
        $block->setTemplate((string)$node['template']);
    }

    if (!empty($node['output'])) {
        $method = (string)$node['output'];
        $this->addOutputBlock($blockName, $method);
    }
    Varien_Profiler::stop($_profilerKey);

    return $this;
}

Firstly, a check is made to see whether the node contains a class attribute and if it does, sets this in a $className variable. If it doesn’t, we set the type attribute to the variable.

if (!empty($node['class'])) {
    $className = (string)$node['class'];
} else {
    $className = (string)$node['type'];
}

More often than not, the $className will be derived from the type. In our case with the formkey block, $className is set to core/template.

We then establish the $blockName by looking at the name attribute of the node, and use both the $className and $blockName and pass them into the addBlock() method.

$blockName = (string)$node['name'];
$_profilerKey = 'BLOCK: '.$blockName;
Varien_Profiler::start($_profilerKey);
$block = $this->addBlock($className, $blockName);

If we use our formkey block, the addBlock() parameters are core/template and formkey.

The addBlock() method simply delegates to the createBlock() method.

public function addBlock($block, $blockName)
{
    return $this->createBlock($block, $blockName);
}

The createBlock() method consists of the following.

public function createBlock($type, $name='', array $attributes = array())
{
    try {
        $block = $this->_getBlockInstance($type, $attributes);
    } catch (Exception $e) {
        Mage::logException($e);
        return false;
    }

    if (empty($name) || '.'===$name{0}) {
        $block->setIsAnonymous(true);
        if (!empty($name)) {
            $block->setAnonSuffix(substr($name, 1));
        }
        $name = 'ANONYMOUS_'.sizeof($this->_blocks);
    } elseif (isset($this->_blocks[$name]) && Mage::getIsDeveloperMode()) {
        //Mage::throwException(Mage::helper('core')->__('Block with name "%s" already exists', $name));
    }

    $block->setType($type);
    $block->setNameInLayout($name);
    $block->addData($attributes);
    $block->setLayout($this);

    $this->_blocks[$name] = $block;
    Mage::dispatchEvent('core_layout_block_create_after', array('block'=>$block));
    return $this->_blocks[$name];
}

Firstly, we try and get a block instance of the block name.

try {
    $block = $this->_getBlockInstance($type, $attributes);
} catch (Exception $e) {
    Mage::logException($e);
    return false;
}

The _getBlockInstance() method consists of the following.

protected function _getBlockInstance($block, array $attributes=array())
{
    if (is_string($block)) {
        if (strpos($block, '/')!==false) {
            if (!$block = Mage::getConfig()->getBlockClassName($block)) {
                Mage::throwException(Mage::helper('core')->__('Invalid block type: %s', $block));
            }
        }
        if (class_exists($block, false) || mageFindClassFile($block)) {
            $block = new $block($attributes);
        }
    }
    if (!$block instanceof Mage_Core_Block_Abstract) {
        Mage::throwException(Mage::helper('core')->__('Invalid block type: %s', $block));
    }
    return $block;
}

Here there are various checks including whether or not the $block (block type) is a string and contains a /.

The line:

Mage::getConfig()->getBlockClassName($block)

Will attempt to find a class name for the block. The getBlockClassName() method is a part of the Magento Factory Methods article if you require to read more about it.

With our form key block example, the core/template block type converts into the Mage_Core_Block_Template class, which is then instantiated if Magento can find a class for this.

if (class_exists($block, false) || mageFindClassFile($block)) {
    $block = new $block($attributes);
}

Returning back to the createBlock() method, we then check to see if a name attribute exists for our block. If it doesn’t we set an anonymous flag against it.

if (empty($name) || '.'===$name{0}) {
    $block->setIsAnonymous(true);
    if (!empty($name)) {
        $block->setAnonSuffix(substr($name, 1));
    }
    $name = 'ANONYMOUS_'.sizeof($this->_blocks);
} elseif (isset($this->_blocks[$name]) && Mage::getIsDeveloperMode()) {
    //Mage::throwException(Mage::helper('core')->__('Block with name "%s" already exists', $name));
}

We then various data against the block and add the block to a _blocks property.

$block->setType($type);
$block->setNameInLayout($name);
$block->addData($attributes);
$block->setLayout($this);

$this->_blocks[$name] = $block;
Mage::dispatchEvent('core_layout_block_create_after', array('block'=>$block));
return $this->_blocks[$name];

After all that, we can now return to the _generateBlock() method. The next step checks if to see a parent node exists and whether or not the nodes contain a before or after attribute.

if (!empty($node['parent'])) {
    $parentBlock = $this->getBlock((string)$node['parent']);
} else {
    $parentName = $parent->getBlockName();
    if (!empty($parentName)) {
        $parentBlock = $this->getBlock($parentName);
    }
}
if (!empty($parentBlock)) {
    $alias = isset($node['as']) ? (string)$node['as'] : '';
    if (isset($node['before'])) {
        $sibling = (string)$node['before'];
        if ('-'===$sibling) {
            $sibling = '';
        }
        $parentBlock->insert($block, $sibling, false, $alias);
    } elseif (isset($node['after'])) {
        $sibling = (string)$node['after'];
        if ('-'===$sibling) {
            $sibling = '';
        }
        $parentBlock->insert($block, $sibling, true, $alias);
    } else {
        $parentBlock->append($block, $alias);
    }
}

Specifics of the sorting of the nodes won’t be touched upon in detail, but any blocks with a before="-" attribute will be ordered first, any with a after="-" attribute will be ordered last, and any before/after attributes that contain a block name value, i.e. before="someblockname, will be ordered before or after that particular block name.

Finally, we check to see if a template or output attribute exists for the node.

if (!empty($node['template'])) {
    $block->setTemplate((string)$node['template']);
}

if (!empty($node['output'])) {
    $method = (string)$node['output'];
    $this->addOutputBlock($blockName, $method);
}

If a template exists, we set the template file for the node. If an output attribute exists, we add the name of the method of the attribute (i.e. output="toHtml", the method is toHtml) into an _output array property.

This _output property is important to remember as it will be called upon in the renderLayout() method.

Usually within Magento’s layout files you’ll find root named blocks have an output attribute of toHtml such as the one in page.xml.

<?xml version="1.0"?>
<layout version="0.1.0">
<!--
Default layout, loads most of the pages
-->

    <default translate="label" module="page">
        <label>All Pages</label>
        <block type="page/html" name="root" output="toHtml" template="page/3columns.phtml">
            ....
        </block>
        ....
    </default>
    ....
</layout>

Going back to the generateBlocks() method, we’ve looked at the _generateBlock() method but haven’t looked at the _generateAction() method that gets called if our node name is action.

protected function _generateAction($node, $parent)
{
    if (isset($node['ifconfig']) && ($configPath = (string)$node['ifconfig'])) {
        if (!Mage::getStoreConfigFlag($configPath)) {
            return $this;
        }
    }

    $method = (string)$node['method'];
    if (!empty($node['block'])) {
        $parentName = (string)$node['block'];
    } else {
        $parentName = $parent->getBlockName();
    }

    $_profilerKey = 'BLOCK ACTION: '.$parentName.' -> '.$method;
    Varien_Profiler::start($_profilerKey);

    if (!empty($parentName)) {
        $block = $this->getBlock($parentName);
    }
    if (!empty($block)) {

        $args = (array)$node->children();
        unset($args['@attributes']);

        foreach ($args as $key => $arg) {
            if (($arg instanceof Mage_Core_Model_Layout_Element)) {
                if (isset($arg['helper'])) {
                    $helperName = explode('/', (string)$arg['helper']);
                    $helperMethod = array_pop($helperName);
                    $helperName = implode('/', $helperName);
                    $arg = $arg->asArray();
                    unset($arg['@']);
                    $args[$key] = call_user_func_array(array(Mage::helper($helperName), $helperMethod), $arg);
                } else {
                    /**
                     * if there is no helper we hope that this is assoc array
                     */
                    $arr = array();
                    foreach($arg as $subkey => $value) {
                        $arr[(string)$subkey] = $value->asArray();
                    }
                    if (!empty($arr)) {
                        $args[$key] = $arr;
                    }
                }
            }
        }

        if (isset($node['json'])) {
            $json = explode(' ', (string)$node['json']);
            foreach ($json as $arg) {
                $args[$arg] = Mage::helper('core')->jsonDecode($args[$arg]);
            }
        }

        $this->_translateLayoutNode($node, $args);
        call_user_func_array(array($block, $method), $args);
    }

    Varien_Profiler::stop($_profilerKey);

    return $this;
    }

One of the first nodes that enters this method is the addJs method that adds the prototype.js file.

<?xml version="1.0"?>
<layout version="0.1.0">
<!--
Default layout, loads most of the pages
-->

    <default translate="label" module="page">
        <label>All Pages</label>
        <block type="page/html" name="root" output="toHtml" template="page/3columns.phtml">

            <block type="page/html_head" name="head" as="head">
                <action method="addJs"><script>prototype/prototype.js</script></action>
                ....
            </block>
            ....
        </block>
        ....
    </default>
    ....
</layout>

The first part of the _generationAction() method checks for an ifconfig flag set on the node. Depending on whether a configuration flag in the admin area is set to true or false determines whether or not to proceed any further with the method.

if (isset($node['ifconfig']) && ($configPath = (string)$node['ifconfig'])) {
    if (!Mage::getStoreConfigFlag($configPath)) {
        return $this;
    }
}

$method = (string)$node['method'];
if (!empty($node['block'])) {
    $parentName = (string)$node['block'];
} else {
    $parentName = $parent->getBlockName();
}

We can try and establish the parent name of the node. With our addJs example, the parent block name is head as seen in the layout code snippet above.

We then get the block class by passing the parent name into the getBlock() method.

if (!empty($parentName)) {
    $block = $this->getBlock($parentName);
}

This will give us Mage_Page_Block_Html_Headaction node contains a helper attribute. Sometimes this is present within the addLink() action method, such as when Magento adds its top links, like the My Account link within the customer.xml layout file.

<?xml version="1.0"?>
<layout version="0.1.0">

<!--
Default layout, loads most of the pages
-->

    <default>
        <!-- Mage_Customer -->
        <reference name="top.links">
            <action method="addLink" translate="label title" module="customer"><label>My Account</label><url helper="customer/getAccountUrl"/><title>My Account</title><prepare/><urlParams/><position>10</position></action>
        </reference>
    </default>
</layout>

Either way of a helper attribute exists or not, we add the node arguments to an $args array and pass out method name and the array into the call_user_func_array() function which executes the method.

call_user_func_array(array($block, $method), $args);

Note: This article is based on Magento Community/Open Source version 1.9.