Categories


Archives


Recent Posts


Categories


Magento Block Lifecycle Methods

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!

This entry is part 25 of 43 in the series Miscellaneous Magento Articles. Earlier posts include Magento Front Controller, Reinstalling Magento Modules, Clearing the Magento Cache, Magento's Class Instantiation Abstraction and Autoload, Magento Development Environment, Logging Magento's Controller Dispatch, Magento Configuration Lint, Slides from Magento Developer's Paradise, Generated Magento Model Code, Magento Knowledge Base, Magento Connect Role Directories, Magento Base Directories, PHP Error Handling and Magento Developer Mode, Magento Compiler Mode, Magento: Standard OOP Still Applies, Magento: Debugging with Varien Object, Generating Google Sitemaps in Magento, IE9 fix for Magento, Magento's Many 404 Pages, Magento Quickies, Commerce Bug in Magento CE 1.6, Welcome to Magento: Pre-Innovate, Magento's Global Variable Design Patterns, and Magento 2: Factory Pattern and Class Rewrites. Later posts include Goodnight and Goodluck, Magento Attribute Migration Generator, Fixing Magento Flat Collections with Chaos, Pulse Storm Launcher in Magento Connect, StackExchange and the Year of the Site Builder, Scaling Magento at Copious, Incremental Migration Scripts in Magento, A Better Magento 404 Page, Anatomy of the Magento PHP 5.4 Patch, Validating a Magento Connect Extension, Magento Cross Area Sessions, Review of Grokking Magento, Imagine 2014: Magento 1.9 Infinite Theme Fallback, Magento Ultimate Module Creator Review, Magento Imagine 2014: Parent/Child Themes, Early Magento Session Instantiation is Harmful, Using Squid for Local Hostnames on iPads, and Magento, Varnish, and Turpentine.

There’s two design patterns used in PHP systems that are often conflated. First, there’s the event/observer pattern. This pattern allows a system-developer to issue global events which client-developers may then setup listener object/methods/functions for. These listeners observe the event, and then perform some action. This keeps client code out of system code, and the OOP gods are happy.

The second pattern is often called, confusingly, the event listener pattern. I’ve also seen it called the callback pattern. This pattern is used when a client-developer is defining classes which extend base classes provided by the system developers. The classic example is an ActiveRecord/CRUD model. Imagine a simple ORM where you define a model, something like this

class Car extends BaseModel
{

}

Most ORMs provide an afterSave method of some kind

class Car extends BaseModel
{
    protected function _afterSave()
    {
    }
}

This theoretical _afterSave method is the event listener. It’s conceptually similar to the event/observer pattern, but its context and implementation is very different. If we’re talking classic OOP design patterns, this is the template method pattern (thanks Stack Overflow).

In my own out I’ve often called these sorts of methods “lifecycle callbacks”, because they’re template methods that relate directly to certain stages in a block’s life. That’s what we’ll be calling them for the remainder of this article.

Magento’s Lifecycle Callbacks

Magento is filled with all sorts of these callbacks. For example, Magento’s CRUD/EAV models have their own version of the before/after save methods common in most ORMs. Another place you see the pattern is a controller’s preDispatch method. A third system that’s littered with these calls is the layout/block system. Magento’s unique approach to layout generation means developers often have trouble understanding all the callback methods available to them. Today we’ll be reviewing these methods: What they do, how they’re implemented, and how some will behave in unexpected ways.

While not required reading, owners of No Frills Magento Layout will feel right at home.

The Six Block Lifecycle Callbacks

To start we’re going to briefly describe each of the methods we’re singling out as lifecycle callbacks. Definitions get fuzzy here because while a client-developer might describe these methods as listeners, to a systems developer they’re just solid OOP. Any method is a potential “callback” when you’re intimately familiar with the base class.

Callback: _construct()

The _construct method is Magento’s internal (or “pseudo”) constructor. All objects that inherit from Varien_Object (which includes blocks) have this callback, which is called when you instantiate a block. This method isn’t specific to blocks, but its behavior is close enough to warrant its inclusion in our lifecycle callback lineup.

Callback: _prepareLayout()

The _prepareLayout() method is called immediately after a block has been added to the layout object for the first time. If this seems vague don’t worry, we’ll get to some specifics in a moment.

Callback: _beforeToHtml()

The _beforeToHtml() method is called immediately before a block’s HTML content is rendered.

Callback: _afterToHtml($html)

The _afterToHtml method is called immediately after a block’s HTML content is generated. It’s also the only method with both a method parameter and a required return value. Whatever content is returned from your _afterToHtml method call will become the rendered content for that block. So the following

class Packagename_Namespace_Block_Model extends Mage_Core_Block_Template
{
    protected function _afterToHtml($html)
    {
        return $html . '<div>Killroy was here</div>';
    }
}

would automatically add the text

<div>Killroy was here</div>

to the end of your block when it rendered. However, failure to return a value

class Packagename_Namespace_Block_Model extends Mage_Core_Block_Template
{
    protected function _afterToHtml($html)
    {
        Mage::Log("I just rendered " . $this->getName());
    }
}

would result in an empty block being rendered. Make sure you don’t forget your return value if you’re using _afterToHtml.

Callback: _toHtml()

The _toHtml method is another questionable inclusion in the lifecycle lineup. The _toHtml method is where you put the code that should render your block. You’ll see a little later why we’re including this as a lifecycle callback.

Callback: _beforeChildToHtml($name, $block)

Finally, there’s the little known _beforeChildToHtml method. In Magento, a layout is a nested tree structure of blocks, with parent blocks rendering child blocks. When a parent renders one of its children (through a call to $this->getChildHtml('name')), this method is called immediately before rendering the child.

This method isn’t in use anywhere in core Magento code, so it’s likely one of those legacy methods they’re keeping around for backward compatibility. You’ll probably never encounter it, but it’s worth knowing about.

Creating a Block

Before we jump into the implementation details, we’re going to take a look at the ultimate lifecycle callback, and one we didn’t mention above: PHP’s constructor method. In you’re brand new to PHP, whenever you instantiate an object from a class, that class’s __construct method is called.

$o = new SomeObject(1,2,3);
class SomeObject
{
    public function __construct($parm,$second,$etc)
    {

    }
}

PHP classes also have an older, but still supported, feature where a method with the same name as the class will be used as the constructor.

$o = new SomeObject(1,2,3);
class SomeObject
{
    //still a constructor
    public function SomeObject($parm,$second,$etc)
    {

    }
}

We mention this mainly in the interest of completeness, as the __construct format is the preferred technique for defining a PHP constructor, and the “same name” functionality has been dropped from code using namespaces.

If you examine at the definition of an abstract block class (the class all proper blocks inherit from)

#File: app/code/core/Mage/Core/Block/Abstract.php    
abstract class Mage_Core_Block_Abstract extends Varien_Object
{
    //...    
}

you’ll see there’s no constructor (of either style) defined. This abstract class extends the base Varien_Object class, so we should inspect that class for a constructor, as PHP will climb the ancestry chain until it finds one to call.

#File: lib/Varien/Object.php
class Varien_Object implements ArrayAccess
{
    //...
    public function __construct()
    {
        $this->_initOldFieldsMap();
        if ($this->_oldFieldsMap) {
            $this->_prepareSyncFieldsMap();
        }

        $args = func_get_args();
        if (empty($args[0])) {
            $args[0] = array();
        }
        $this->_data = $args[0];
        $this->_addFullNames();

        $this->_construct();
    }
    //...
}       

This is the constructor that’s shared by all objects that inherit from Varien_Object. It’s also the home of the first implemented Magento lifecycle callback, the _construct method.

By placing the following code at the end of the PHP constructor

$this->_construct();

Magento gives client developers the ability to define a _construct method in their own classes.

class Packagename_Namespace_Block_Model extends Mage_Core_Block_Abstract
{
    public function _construct()
    {
        //do your own constructor stuff here
    }
}

Why create a separate system for constructors instead of relying on the built in method? There’s lots of reasons system developers like doing this, but most of them boil down to control.

By providing an internal _construct method, the core team is giving client developers the same functionality as PHP’s constructor while simultaneously claiming the native PHP constructor for themselves.

Consider a block like this, which is (incorrectly) using PHP’s constructor for some block initialization

class Packagename_Namespace_Block_Model extends Mage_Core_Block_Abstract
{
    public function __construct()
    {
        $this->setTemplate('path/to/the.phtml');
    }
}

See anything wrong? No? Well, for one there’s no call to the parent constructor, meaning the Varien_Object code never runs. Failure to call, or realize you’re supposed to call, the parent constructor is common among users of PHP frameworks. When this happens, you end up with a block object that behaves slightly differently than a standard block object that’s had its Varien_Object::__construct code called. This is a “bad thing” from a system developer’s point of view, and can result in inconsistent/confusing system behavior.

Even if an OOP savvy developer did call it

public function __construct()
{            
    parent::__construct();
    $this->setTemplate('path/to/the.phtml');            
}

there’s still a problem. The client developer has created a constructor prototype and parent call with no arguments. This is perfectly valid PHP, but if we look at the factory instantiation code for a block

#File: app/code/core/Mage/Core/Model/Layout.php
$block = new $block($attributes);

we see that Magento’s system code passes in an argument to the constructor. With the __construct using block above, those arguments don’t get passed through. However, the Varien_Object constructor expects those arguments

#File: lib/Varien/Object.php
public function __construct()
{
    //...snip...        
    $args = func_get_args();
    if (empty($args[0])) {
        $args[0] = array();
    }
    //...snip...
}
//...

So, again, we have a block that behaves differently than other system blocks, hurting system consistency. By creating their own internal _construct for users, the core team avoids this entire problem while still giving developers 99% of what they need from a constructor.

When is a Block Created

Above we briefly discussed the code Magento’s block factory method uses. Next we’re going to dive a bit deeper into the createBlock method, and how it ties into the _prepareLayout lifecycle callback.

To start, let’s review the various ways a block might be instantiated. The first form, and one that’s discouraged, would be by direct class instantiation

$block = new Packagename_Modulename_Block_Foo;

Here we’re using basic PHP to create an instance of a block.

A more knowledgeable developer might try something like this.

$class = Mage::getConfig()->getBlockClassName('groupname/foo');    
$block = new $class;

Here we’re using the class alias groupname/foo to lookup the proper class name, and then instantiating that.

A third, and recommended, method is to use the layout object’s createBlock factory method

$layout = Mage::getSingleton('core/layout');
$block  = $layout->createBlock('groupname/foo');

//in a controller action context (or other class with a getLayout method)
$block  = $this->getLayout()->createBlock('groupname/foo');

In addition to ensuring instantiation passes through Magento’s class alias/rewrite system, the createBlock method also ensures that a block is associated with the layout. (Yes, that’s vague. Patience Luke!)

The fourth method of creating a block is via layout update XML. When the Layout generating system encounters code like this

<block type="groupname/foo" name="baz" />

a block will be immediately instantiated via the createBlock method, so the above is roughly equivalent to

$layout = Mage::getSingleton('core/layout');
$block  = $layout->createBlock('groupname/foo','baz');

meaning layout update XML created blocks also pass through the createBlock method.

Neighborhood Block Association

Earlier we said blocks created by the createBlock method are “associated” with the layout. Here’s what associated meant. If you look at this small slice of the createBlock method in the layout object

#File: app/code/core/Mage/Core/Model/Layout.php
public function createBlock($type, $name='', array $attributes = array())
{       
    //...
    //$block is the instantiated block object at this point

    $block->setLayout($this);
    //...
}

you can see that after Magento instantiates a block we’re calling that block’s setLayout method and passing in a reference to the layout object ($this). This is not a magic set method, so lets take a look at its definition in setLayout

#File: app/code/core/Mage/Core/Block/Abstract.php   
public function setLayout(Mage_Core_Model_Layout $layout)
{
    $this->_layout = $layout;
    Mage::dispatchEvent('core_block_abstract_prepare_layout_before', array('block' => $this));
    $this->_prepareLayout();
    Mage::dispatchEvent('core_block_abstract_prepare_layout_after', array('block' => $this));
    return $this;
}

After this method has done the job of setting the layout object

#File: app/code/core/Mage/Core/Block/Abstract.php

$this->_layout = $layout;

we can see it also makes a call to _prepareLayout

#File: app/code/core/Mage/Core/Block/Abstract.php

$this->_prepareLayout();

Ah ha! There’s our next lifecycle callback. The _prepareLayout method allows a client-developer to hook into the point in time when a block is being added to a layout

class Packagename_Namespace_Block_Model extends Mage_Core_Block_Abstract
{
    protected function _prepareLayout()
    {
        //do stuff after you've been added to the layout
    }
}

While this happens at a stage of the block’s lifecycle that’s very close to the _construct method, it only happens for block objects that have been added to the layout. If a block’s instantiated in a way that doesn’t involve createBlock or setLayout, this lifecycle callback will not be called, while the _construct method will always be called.

This means it’s safe to assume to existence of a layout object in your _prepareLayout method, and if your block needs to manipulate other blocks to achieve its ends you can safely call methods on the layout object. If you tried this in _construct, someone using the block outside of a layout (rare, but it does happen), might run into fatal errors.

One last thing to note before we move on. Notice the call to _prepareLayout is nestled in-between two calls to Mage::dispatch

#File: app/code/core/Mage/Core/Block/Abstract.php

Mage::dispatchEvent('core_block_abstract_prepare_layout_before', array('block' => $this));
$this->_prepareLayout();
Mage::dispatchEvent('core_block_abstract_prepare_layout_after', array('block' => $this));

The dispatchEvent method is part of the event/observer pattern. We mention it mainly so you’re aware that your own _prepareLayout code will be called after the core_block_abstract_prepare_layout_before global system observers have had a chance to muck about with the block, and that further changes can be made to the block with the core_block_abstract_prepare_layout_after global system observers. If your code in _prepareLayout isn’t working as you expect, make sure there’s not a global listener that’s silently confusing you.

Rendering HTML

So far we’ve covered lifecycle callbacks that are triggered during block instantiation. There are no further callbacks until its time to render a block’s html. The Magento system code renders block by calling the toHtml method.

This toHtml method is, and always will be, defined on the base abstract block class

#File: app/code/core/Mage/Core/Block/Abstract.php
final public function toHtml()
{
    ...
}

When we say always will be, we mean it. Notice this method is defined with the final keyword. This means no child class is allowed to redefine its own toHtml method. This is another example of system developers ensuring they own a certain section of the system, and that the code in the toHtml method will always be called.

We’re not going to go completely into how a block is rendered. Instead, we’ll want to pay attention to the following code slice in the toHtml method.

#File: app/code/core/Mage/Core/Block/Abstract.php
$html = $this->_loadCache();
if ($html === false) {
    //...snip...

    $this->_beforeToHtml();
    $html = $this->_toHtml();
    $this->_saveCache($html);

    //...snip...
}
$html = $this->_afterToHtml($html);

The method that actually does the work (creating the PHP string) of rendering a block is the _toHtml method. Notice the leading underscore. That’s what the $html = $this->_toHtml(); is all about.

Earlier we identified _toHtml as a lifecycle callback, although it’s a callback in same way a model object’s save method is a callback. Your custom block can tap into it, but unless you’re creating a new block type you’ll almost always want to call the parent method, as that’s what does the actual rendering

public function _toHtml()
{
    $html = parent::_toHtml();
    //more stuff here
    return $html;
}

You can also see the implementation of the _beforeToHtml callback, made immediately before the _toHtml method.

#File: app/code/core/Mage/Core/Block/Abstract.php
$this->_beforeToHtml();
$html = $this->_toHtml();

More interesting though it the entire conditional block surrounding these two method calls

#File: app/code/core/Mage/Core/Block/Abstract.php
$html = $this->_loadCache();
if ($html === false) {
    ...
    $this->_beforeToHtml();
    $html = $this->_toHtml();            
}

Prior to rendering the layout, Magento checks the cache for any rendered output. If Magento loads an entry from the cache (that is, if _loadCache returns something other than boolean false), both the _toHtml and _beforeToHtml calls are skipped. This can trip people up who want their block to do something extra on every page and unwittingly put their code in a _beforeToHtml method.

Contrast this with the implementation of the _afterToHtml() method.

#File: app/code/core/Mage/Core/Block/Abstract.php
$html = $this->_loadCache();
if ($html === false) {
    //...
}
$html = $this->_afterToHtml($html);

This happens outside the cache checking block. This means if a block is being actively rendered, both the _beforeToHtml and _afterToHtml methods will be called but if a block’s content is being pulled from cache, only the _afterToHtml method will be called.

Global Event Equivalents

Just as with the _prepareLayout method, the before and after html rendering methods have analogous global system events

core_block_abstract_to_html_before
core_block_abstract_to_html_after

Unlike the lifecycle callbacks, these events will fire regardless of the cache state of a block. So, if you were trying to use _beforeToHtml but its cache behavior was problematic, setting a listener for core_block_abstract_to_html_before should get you where you need to go.

Rendering Child Blocks

Our final lifecycle callback requires a quick layout/block primer. As we said previously, a Magento layout object is a nested tree of block objects. That is, an individual block in the layout may have multiple child block. This individual block is the parent block, and is responsible for rendering the child blocks. The call you make to render a child block looks like this

$this->getChildHtml(); //renders all children
$this->getChildHtml('block_name') //renders the block_name block

The _beforeChildToHtml method is one you define on the parent block, and it will be passed the name and instance of every child block it renders.

This seems straight forward enough, but there’s a few quirks to be aware of. The best way to understand these quirks is to examine the getChildHtml implementation in full. If we take a look at the method definition

#File: app/code/core/Mage/Core/Block/Abstract.php
public function getChildHtml($name = '', $useCache = true, $sorted = false)
{
    if ($name === '') {
        if ($sorted) {
            $children = array();
            foreach ($this->getSortedChildren() as $childName) {
                $children[$childName] = $this->getLayout()->getBlock($childName);
            }
        } else {
            $children = $this->getChild();
        }
        $out = '';
        foreach ($children as $child) {
            $out .= $this->_getChildHtml($child->getBlockAlias(), $useCache);
        }
        return $out;
    } else {
        return $this->_getChildHtml($name, $useCache);
    }
}

we see there’s two top level code paths. The first is for a call with no $name parameter, which renders all the children. The second is for a call with a name parameter. However, both leafs lead to a call to _getChildHtml

$this->_getChildHtml($name, $useCache);
$out .= $this->_getChildHtml($child->getBlockAlias(), $useCache);

And here’s the method definition for _getChildHtml

#File: app/code/core/Mage/Core/Block/Abstract.php
protected function _getChildHtml($name, $useCache = true)
{
    if ($useCache && isset($this->_childrenHtmlCache[$name])) {
        return $this->_childrenHtmlCache[$name];
    }

    $child = $this->getChild($name);

    if (!$child) {
        $html = '';
    } else {
        $this->_beforeChildToHtml($name, $child);
        $html = $child->toHtml();
    }

    $this->_childrenHtmlCache[$name] = $html;
    return $html;
}

The first quirk you’ll want to be aware of is the $useCache parameter. You might think a client developer uses this to control whether or not a call loads at item from Magento’s global block output cache, or generates the block anew. However, this is not what this parameter is for.

Instead, every block keeps a cache of output in the object property _childrenHtmlCache. This is a per-http-request cache. If the $useCache parameter is true, Magento will check this internal property for an already generated output block before calling the toHtml method on the child block

#File: app/code/core/Mage/Core/Block/Abstract.php
if ($useCache && isset($this->_childrenHtmlCache[$name])) {
return $this->_childrenHtmlCache[$name];
}

Assuming the output was not found in the cache array, Magento will fetch the specified child from the layout.

#File: app/code/core/Mage/Core/Block/Abstract.php
$child = $this->getChild($name);

and then, if there’s a child with the specified name, Magento will render the block. Otherwise, it will set the HTML output to a blank string

#File: app/code/core/Mage/Core/Block/Abstract.php
if (!$child) {
    $html = '';
} else {
    $this->_beforeChildToHtml($name, $child);
    $html = $child->toHtml();
}

Finally, the generated output will be returned right after Magento stashes the output in the aforementioned _childrenHtmlCache cache.

#File: app/code/core/Mage/Core/Block/Abstract.php
$this->_childrenHtmlCache[$name] = $html;
return $html;

This implementation leads to a few quirks. First, as the $useCache method defaults to true, _beforeChildToHtml method will only be called the first time a specific individual parent block renders a child block. If the parent block calls getChildHtml again with the same block _beforeChildToHtml won’t be called. However, if a different parent block calls getChildHtml using the same block name (possible if someone used unsetChild to move a block), _beforeChildToHtml will be called a second time.

Another quirk worth noting is that unlike the other lifecycle callbacks, there’s no global Magento event equivalent for _beforeChildToHtml or an _after method. These omissions, plus the core system code not using this method at all lead me to believe this callback — while still technically supported — is an early concept that the core team has since abandoned. Unless there’s no other way to implement your feature, I’d stay away from this particular callback.

Wrap Up

Lifecycle callback “listeners” can be a useful tool for client developers. However, as you can see, when these callback methods become part of the core system code, it becomes tempting for the system-developers to tweak their implementation is ways that serve their own needs, but aren’t always immediately intuitive to those same client developers.

Because this callback pattern was an early part of PHP 4 MVC systems, there’s a historical cultural expectation that any new system will have similar callbacks. However, as PHP 5 continues its evolution and object oriented PHP becomes the de-facto professional standard, the drawbacks of in-object listener methods start to outweigh the benefits. If you and your team are capable of understanding and debugging the observer/listener patterns available in Magento, I’d consider sticking to them exclusively. They’re the better, more stable choice for implementing additional functionality in your modules.

Originally published March 27, 2012
Series Navigation<< Magento 2: Factory Pattern and Class RewritesGoodnight and Goodluck >>

Copyright © Alana Storm 1975 – 2023 All Rights Reserved

Originally Posted: 27th March 2012

email hidden; JavaScript is required