Reputation: 4549
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
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
Reputation: 546
Another solution, that is, from my point of view, more in the Magento's Spirit is to declare our own handle.
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>
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);
}
}
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>
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 :
To view them, you can temporarily put this at the end of your index.php :
var_dump(Mage::app()->getLayout()->getUpdate()->getHandles());
Upvotes: 4
Reputation: 166046
If you want to change template of a block from an observer, you should
Listen for the controller_action_layout_generate_blocks_after
event
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
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
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