R T
R T

Reputation: 4549

update layout programmatically in magento event observer

I am trying to change the template(view.phtml) of a block (product.info) for product detail page, to do this, I am observing an event (controller_action_layout_generate_blocks_before), in it after making necessary checks I am trying to change the template of the block (product.info) in following way:

$layout = $observer->getEvent()->getLayout();
$layout->getUpdate()->addUpdate('
        <reference name="product.info">
            <action method="setTemplate">
                <template>customlayout/product/view.phtml</template>
            </action>                                                          
        </reference>');
$layout->getUpdate()->load();
$layout->generateXml();

If I put "<remove name='product.info'/>" , it will be removed but when trying to do the above, its not working.
Edit:
Requirement is to switch the template (product detail) dynamically to the selected one (in CustomModule) against the current product.

Upvotes: 9

Views: 29044

Answers (5)

Rodney
Rodney

Reputation: 28

I was going to comment on the fantastic answer by JBreton, but my particular use case which brought me to this thread is slightly different. (Also I'm an SO lurker and do not have adequate reputation to comment yet.)

The accepted answer and other suggestions for modifying the layout in PHP code did not work for me, even after trying to observe various events, so I figured I'd post a steal/support/example answer on JBreton's side. My use case was to REMOVE blocks (core and custom module blocks) from the checkout_cart_index layout programmatically based on certain conditions. The method of using a custom layout handle works for ADDING blocks as well since it simply "activates" a new handle that Magento will process from a standard layout XML file in a theme.

JBreton's method is the BEST from all of the ones that I tried. It makes more sense in the respect of current and future needs. Especially in the case where designers and template builders are not the same people who should be nosing around in the PHP code. Template people know XML and should be well familiar with Magento's layout XML system anyways. So using a custom handle to modify layouts on specific programmatic conditions is the superior method than adding XML through a string in PHP.

AGAIN ... this is not a solution I conjured on my own ... I stole this from JBreton's answer above and am supplying example code which my doppelganger could use in their situation as an additional starting point. Note that not all of my module code is included here (notably the app/modules XML file, model classes, etc).

My module's config file:

app/code/local/Blahblah/GroupCode/etc/config.xml

<config>
  ... other config XML too ...

  <frontend>
    <events>
        <controller_action_layout_load_before>
            <observers>
                <blahblah_groupcode_checkout_cart_index>
                    <type>singleton</type>
                    <class>Blahblah_Groupcode_Model_Ghost</class>
                    <method>checkout_cart_prepare</method>
                </blahblah_groupcode_checkout_cart_index>
            </observers>
        </controller_action_layout_load_before>
    </events>
  </frontend>
</config>

The observer's method in the class:

app/code/local/Blahblah/GroupCode/Model/Observer.php

<?php

    public function checkout_cart_prepare(Varien_Event_Observer $observer)
    {
        // this is the only action this function cares to work on
        $fullActionName = 'checkout_cart_index';

        ... some boring prerequiste code ...

        // find out if checkout is permitted
        $checkoutPermitted = $this->_ghost_checkoutPermitted();

        if(!$checkoutPermitted)
        {
            // add a custom handle used in our layout update xml file
            Mage::app()->getLayout()->getUpdate()->addHandle($fullActionName . '_disable_checkout');
        }

        return $this;
    }

The layout update inclusion in the theme file:

app/design/PACKAGE/THEME/etc/theme.xml

<?xml version="1.0"?>
<theme>
    <parent>...</parent>

    <layout>
        <updates>
            <!-- Adding references to updates in separate layout XML files. -->
            <blahblah_checkout_cart_index>
                <file>blahblah--checkout_cart_index.xml</file>
            </blahblah_checkout_cart_index>

            ... other update references too ...
        </updates>
    </layout>
</theme>

The layout update definition file:

app/design/PACKAGE/THEME/layout/blahblah--checkout_cart_index.xml

<layouts>
    <checkout_cart_index_disable_checkout>
        <reference name="content">
            <block type="core/template" name="checkout.disabled" as="checkout.disabled" before="-" template="checkout/disabled-message.phtml" />
            <remove name="checkout.cart.top_methods" />
            <remove name="checkout.cart.methods" />
        </reference>
    </checkout_cart_index_disable_checkout>

    ... other layout updates too ...
</layouts>

(Yes, there is other code in my module which watches the checkout process events to ensure that someone doesn't sneak in with a manual URL path. And other checks are in place to truly "disable" the checkout. I'm just showing my example of how to programmatically modify a layout through an observer.)

Upvotes: 1

JBreton
JBreton

Reputation: 546

Another solution, that is, from my point of view, more in the Magento's Spirit is to declare our own handle.

1. Declare an observer of controller_action_layout_load_before

In your module config.xml, under the node config>frontend>events put this code :

<controller_action_layout_load_before>
  <observers>
     <stackoverflow_set_handle>
        <class>stackoverflow_module/observer</class>
        <method>setHandle</method>
     </stackoverflow_set_handle>
  </observers>
</controller_action_layout_load_before>

2. Define your observer

class Stackoverflow_Module_Model_Observer
{
    public function setHandle(Varien_Event_Observer $observer)
    {
        $fullActionName = $observer->getEvent()->getAction()->getFullActionName();
        if (/* Any condition you may want to modify the layout */) {
            Mage::app()->getLayout()->getUpdate()->addHandle('MY_HANDLE_' . $fullActionName);
        }
    }

3. Create a layout xml file

Once done, you have any fullActionName available to use as second level node in your layout update files prefixed by MY_HANDLE_.

Theses instructions will be only triggered if the handle is present, so basicly for any condition you have set in your observer.

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

    <MY_HANDLE_catalogsearch_result_index>
        <reference name="left">
            <remove name="catalogsearch.leftnav" />
        </reference>
    </MY_HANDLE_catalogsearch_result_index>

    <MY_HANDLE_catalog_product_view>
        <!-- Do anything you want -->
    </MY_HANDLE_catalog_product_view>

</layout>

Last words

You can of course test the $fullActionName within your observer to have your handle added more specifically, and you can build a handle not dynamically based on fullActionName.

For information, this is the way Magento manages a lot of layout variations :

  • STORE_default > Built dynamically with the current store
  • THEME_frontend_enterprise_enterprise > Built dynamically with the current theme
  • PRODUCT_TYPE_simple > Built dynamically with the current product type
  • PRODUCT_16 > Built dynamically with the current product id
  • customer_logged_out > Only present if customer is logged in
  • and others...

To view them, you can temporarily put this at the end of your index.php :

var_dump(Mage::app()->getLayout()->getUpdate()->getHandles());

Upvotes: 4

Alana Storm
Alana Storm

Reputation: 166046

If you want to change template of a block from an observer, you should

  1. Listen for the controller_action_layout_generate_blocks_after event

  2. Use PHP to manipulate the layout

By listening for the generate after event, you ensure every action method specified via a file based Layout Update XML string will be called first, and your template change will "win".

I recommend using PHP code because the Layout Update XML system is a domain specific language, the intent of which was to provide a limited set of functionality for layout updates without having to write a single line of PHP. If you're already using a PHP observer, it just makes sense to manipulate the layout via PHP.

Code something like this should get you what you want (again, from the after observer method)

$controller   = $observer->getAction();

//limit to the product view page 
if($controller->getFullActionName() != 'catalog_product_view')
{
    return;
}

$layout       = $controller->getLayout();
$product_info = $layout->getBlock('product.info');
if(!$product_info)
{
    Mage::log('Could not find product.info block');
    return;
}

$product_info->setTemplate('customelayout/product/view.phtml');

Upvotes: 16

ivantedja
ivantedja

Reputation: 2553

As Ben said, I don't know why you're going to put it on the observer but the problem in your case is the sequence of loadLayout.

You can check your loaded layout xml by using:

Mage::log(Mage::getSingleton('core/layout')->getUpdate()->asString());

Pretty sure your <action method="setTemplate"><template>customelayout/product/view.phtml</template> has been overridden by other setTemplate that's the reason your template is not shown.

Mage_Core_Controller_Varien_Action

public function loadLayout($handles=null, $generateBlocks=true, $generateXml=true)
{
    // if handles were specified in arguments load them first
    if (false!==$handles && ''!==$handles) {
        $this->getLayout()->getUpdate()->addHandle($handles ? $handles : 'default');
    }

    // add default layout handles for this action
    $this->addActionLayoutHandles();

    $this->loadLayoutUpdates(); //in here: $this->getLayout()->getUpdate()->load();

    if (!$generateXml) {
        return $this;
    }
    //event: controller_action_layout_generate_xml_before
    $this->generateLayoutXml(); //in here: $this->getLayout()->generateXml();

    if (!$generateBlocks) {
        return $this;
    }
    //event: controller_action_layout_generate_blocks_before, your observer is located here
    $this->generateLayoutBlocks(); //in here: $this->getLayout()->generateBlocks();
    $this->_isLayoutLoaded = true;

    return $this;
}

So, you're going to modify the xml using event: controller_action_layout_generate_blocks_before.

It means what you need to do is:

//add the update
$layout->getUpdate()->addUpdate('<reference name="product.info"><action method="setTemplate"><template>customelayout/product/view.phtml</template></action></reference>');
//then generate the xml
$layout->generateXml();

What cause your problem is:

$layout->getUpdate()->load();

was called again after

$layout->getUpdate()->addUpdate('<reference name="product.info"><action method="setTemplate"><template>customelayout/product/view.phtml</template></action></reference>');

Though it is better to use event: controller_action_layout_generate_xml_before. So that you don't need to generate your xml twice.

Upvotes: 24

benmarks
benmarks

Reputation: 23205

Why on earth are you doing it this way?

It would be better to use either the local.xml layout file or a layout file declared for a custom module to do this:

<?xml version="1.0" encoding="UTF-8"?>
<layout>
    <catalog_product_view>
        <reference name="product.info">
            <action method="setTemplate">
                <tpl>customelayout/product/view.phtml</tpl>
            </action>
        </reference>
    </catalog_product_view>
</layout>

FYI when a block name is <remove/>ed, no block with that name will be instantiated for any rendering scope which includes that remove directive.

Upvotes: 13

Related Questions