update layout programmatically in magento event observer - magento

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.

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.

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');

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.

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());

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.)

Related

Magento: how can I show all reviews of all products on a cms page

I want to show all my reviews of all the products on a cms page. Does anybody know ho to do this? I'm using Magento 1.4.2
that all depends on what Module you're using to provide that extension to Magento's features. If you look in the folder of the module (let's say its Cmdcentral/Review)
app/code/community/Cmdcentral/Review/
This is where the module resides (it might be in local too)look in the etc for config.xml There will be a section that looks something like this:
<config>
...
<global>
....
<models>
<review>
<class>Cmdcentral_Review_Model</class>
</review>
<review_mysql4>
<class>Cmdcentral_Review_Model_Mysql4</class>
<entities>
<reviews>table_in_database</reviews>
</entities>
</review_mysql4>
</models>
.....
</global>
...
</config>
This will differ depending on what you've got. what's important is the name of the node inside <entities></entities> in my case it is <reviews></reviews>
You can then take a look at the controllers folder for IndexController.php create a new function that looks like this:
public function showallAction(){
$this->loadLayout();
$this->renderLayout();
}
Now you'll have to create a block for this, create a new block in app/code/community/Cmdcentral/Review/Blocks and call it Showall.php
Your block should look something like this:
<?php class Cmdcentral_Review_Block_Showall extends Mage_Core_Block_Template{
public function getAllReviews(){
return Mage::getModel('review/reviews')->getCollection();
}
}
review is module name and reviews is the entity we saw inside the <entities></entities> node in config.xml.
Next we're off to app/design/frontend/ from here, the file we're looking for is most likely in base/default but may also be in another theme's folder. The file we're looking for will be Modulename.xml so in my case it will be app/design/frontend/base/default/layout/Review.xml
Open your layout file, now simply add this inside the <layout></layout> node:
<review_index_showall>
<reference name="content">
<block type="review/showall" name="showall" template="review/showall.phtml"/>
</reference>
<review_index_showall>
This simply tells Magento that when we load our review/index/showall route and access our showallAction() function in our controller, to add our block inside content.
Now the block also has a template="review/showall.phtml" attribute. Go to app/design/frontend/base/default look for a review directory (or whatever the module is called). If it doesn't exist (which I doubt) create it! Inside this create showall.phtml. So now you should have it looking like app/design/frontend/base/default/review/showall.phtml
Open this file and now you create your page. ~Phew!
Just remember to use $this->getAllReviews() to get your review/reviews collection and then simply do something like this:
$reviews = $this->getAllReviews();
foreach($reviews as $review){
echo $review->getData('column_name');
#or
echo $review->getColumnName();
#does the same thing
}
I hope this helps and I didn't make any mistakes. Remember, at first Magento makes you cry, but when you get used to Magento, it's reduced to intermittent whimpers!

How to add JS programmatically in Magento?

I need to add a JS file conditionally and programmatically inside a block file. I tried with these codes:
if (Mage::getStoreConfig('mymodule/settings/enable')) {
$this->getLayout()->getBlock('head')->addJs('path-to-file/file1.js');
} else {
$this->getLayout()->getBlock('head')->addJs('path-to-file/file2.js');
}
However, regardless of what the setting is, none of this file is loaded. I even tried to eliminate the condition and explicitly load one file only, but it still doesn't work. What have I done wrong here?
The issue here is likely one of processing order. My guess is that your PHP code is being evaluated after the head block has been rendered. While your code is successfully updating the head block class instance, it's happening after output has been generated from that instance.
The better solution will be to add the addJs() calls in layout XML so that they will be processed prior to rendering. It would be nice if there were an ifnotconfig attribute, but for now you can use a helper.
Create a helper class with a method which returns the script path based on the config settings, then use this as the return argument.
<?php
class My_Module_Helper_Class extends Mage_Core_Helper_Abstract
{
public function getJsBasedOnConfig()
{
if (Mage::getStoreConfigFlag('mymodule/settings/enable')) {
return 'path-to-file/file1.js';
}
else {
return 'path-to-file/file2.js';
}
}
}
Then in layout XML:
<?xml version="1.0"?>
<layout>
<default>
<reference name="head">
<action method="addJs">
<file helper="classgroup/class/getJsBasedOnConfig" />
<!-- i.e. Mage::helper('module/helper')->getJsBasedOnConfig() -->
</action>
</reference>
</default>
</layout>
$this->getLayout()->getBlock('head')->addJs('path');
Its the right code, search if your path is right.
I know this was asked a long time ago, but in case somebody is looking for this, I would suggest to use this in your local.xml:
<layout>
<default>
<reference name="head">
<action ifconfig="path/to/config" method="addJs">
<script>pathto/file.js</script>
</action>
</reference>
</default>
</layout>
Of course this is for JS files located in /js/ folder. Use the appropriate method if you want to add skin_js or skin_css.
PS. Tested on CE 1.9

Magento changes layout dynamically via system variable

Is there a way we could changes the layout of a Magento page (let's say a product category page) dynamically by using system variable which have been set on our own module? I want to be able to set my category page's default layout via my own module admin config panel. So that I don't have to deal with those confusing XML layout file each time I want to change my default layout for a certain magento page.
I know, on a phtml file, we could simply call our own module's system variable by calling Mage::getStoreConfig('module/scope/...') to use that system variable. but what if we want to use that system variable to change the whole layout which is set on the XML layout file by default.
I don't see any ways to pull that system variable value on the XML Layout file.
But I'm pretty sure there must be a right way to do that. So far, this is the closest clue that I've got
Magento - xml layouts, specify value for ifconfig?
But, still, I couldn't find any direct answer for what I really want to achieve
this is the content of my config.xml
<config>
<modules>
<Prem_Spectra>
<version>0.1.0</version>
</Prem_Spectra>
</modules>
<global>
<models>
<spectra>
<class>Prem_Spectra_Model</class>
</spectra>
</models>
<helpers>
<prem_spectra>
<class>Prem_Spectra_Helper</class>
</prem_spectra>
</helpers>
</global>
</config>
This can be very easily achieved using layout xml and a simple method in your helper. I don't see any requirement for an observer here or anything else overly elaborate.
So, based on your requirements to change all category page layouts from your own modules store config value you will require the following in your layout xml:
<catalog_category_view>
<reference name="root">
<action method="setTemplate">
<template helper="yourmodule/switchTemplate" />
</action>
</reference>
</catalog_category_view>
And the following in your modules default helper:
public function switchTemplate()
{
$template = Mage::getStoreConfig('path_to/yourmodule/config');
return $template;
}
we are talking about the template of the root-element, so 3columns, 2columns, etc? correct?
Implement an observer, listen to the event controller_action_layout_generate_blocks_before and then get the block in the observer and set the template
Mage::app()->getLayout()->getBlock('root')->setTemplate($myFancyTemplatePath);
This should do it.
Other idea, try the event controller_action_layout_load_before, but I think this is too early.
In addition to Fabian's answer:
You could perhaps extend the functionality of the category 'display modes'.
Using the controller_action_layout_load_before event and then retrieve the display mode of the category and create a XML update handle for it.
$category = Mage::registry('current_category');
$handle = 'category_displaymode_' . strtolower($category->getDisplayMode());
$layout = $observer->getEvent()->getLayout();
$layout->getUpdate()->addHandle($handle);
This way you can pre-define all kinds of layouts in your local.xml and easily switch between them by adjusting the 'display mode' dropdown on the category edit page in the admin.
With some tweaking in the admin you can add additional display modes to the dropdown to make more types of custom display mode xml update handles available.

Set Magento block template in layout xml

Having trouble setting a block template in Magento's layout xml. I'm attempting to set the template of a child block, not the entire page layout (almost all docs out there explain how to set template of the layout).
Background: I'm updating a layout handle in my custom action, using the <update /> tag in my module's layout xml.
Essentially, I want to reuse the layout and blocks of the built in product view action, but provide custom templates for a few blocks. (Not just overrides, these need to be brand new templates that are only triggered on my custom action and are themselves overrideable).
My layout html:
<?xml version="1.0"?>
<layout version="0.1.0">
<mymodule_product_index>
<update handle="catalog_product_view" />
<reference name="content">
<block type="catalog/product_view"
name="product.info" output="toHtml" template="mymodule/product.phtml" />
</reference>
<reference name="product.info.bundle">
<action method="setTemplate"><template>mymodule/customtemplate.phtml</template></action>
</reference>
</mymodule_product_index>
</layout>
The setTemplate on product.info.bundle never works; it doesn't seem to affect layout at all. I've tried wrapping the <reference> in other <reference> nodes from parent blocks with no effect. Is it possible to replace block templates in this way? I feel that my problem stems from the fact I'm using an <update />.
By the way, I know my layout xml is being loaded and there are no errors, the rest of the file is working fine, caching is disabled, have cleared cache anyway, etc.
Your approach is almost correct.
Two things:
1. Set a new template instead of instantiating a new block
Instead of just assigning a different template to the product.info block, you are creating a new instance with the same name, replacing the original instance, and then the new template is set on that. Instead use this:
<mymodule_product_index>
<update handle="catalog_product_view" />
<reference name="product.info">
<action method="setTemplate">
<template>mymodule/product.phtml</template>
</action>
</reference>
</mymodule_product_index>
That should take care of the product view template in a clean way.
2. Handle processing order
If you look at where the view block product.info.bundle for the bundled products is declared, you will see it happens in the bundle.xml file, in a layout update handle called <PRODUCT_TYPE_bundle>.
Your code is referencing the block from the <[route]_[controller]_[action]> layout handle, i.e. <mymodule_product_index>.
The thing to be aware of here is the processing order of layout handles.
Roughly it is:
<default>
<[route]_[controller]_[action]>
<custom_handles>
The <PRODUCT_TYPE_bundle> handle belongs to the third type of layout handles, which means it is processed after the <mymodule_product_index> handle.
In essence, you are referencing the block product.info.bundle before it has been declared.
To fix this you will need to use the <PRODUCT_TYPE_bundle> handle as well. Of course this will effect every bundled product display. Using layout XML only there is no clean way around that.
Here are a few suggestions how to solve that problem.
You could create a separate route in your module to show the bundled products, and then include the <PRODUCT_TYPE_bundle> handle using an update directive for that page, too.
In your custom action controller, you could add another layout update handle that is processed after <PRODUCT_TYPE_bundle>.
You could use an event observer to set the template on the product.info.bundle block if it is instantiated. One possibility would be the event controller_action_layout_generate_blocks_after.
You get the idea, there are many ways to work around this, but they require PHP.

Layout Cache Issue

I have a custom module I wrote that is pretty basic...it just adds a small block to the footer for tracking using Media Forge. The tag it adds is different depending on whether you're on a product view page or not. This worked GREAT....until I turned on caching. Now, if you flush the cache and load a product view page, you get the correct block for the product view page. If you then go to another page (home, for instance), it still uses the product view page's block. If I flush the cache and reload the home page, it's now using the right one, but if I go to a product page now, it's using the wrong one there. So it's definitely a cache issue, I just don't understand how I'm supposed to correct this problem.
I'll paste the contents of my files below. I look forward to any responses!
Layout XML file:
<layout version="0.1.0">
<!-- DEFAULT TAG -->
<default>
<reference name="footer">
<block type="core/template" name="mediaforge_footer" as="mediaforge_footer" template="tracking/mediaforge_default.phtml"/>
</reference>
</default>
<!-- PRODUCT VIEW PAGES -->
<catalog_product_view>
<reference name="mediaforge_footer">
<action method="setTemplate"><template>tracking/mediaforge_product.phtml</template></action>
</reference>
</catalog_product_view>
</layout>
config.xml for my custom module:
<config>
<modules>
<VPS_Tracking>
<version>0.1.0</version>
</VPS_Tracking>
</modules>
<frontend>
<layout>
<updates>
<vps_tracking>
<file>vps_tracking.xml</file>
</vps_tracking>
</updates>
</layout>
</frontend>
</config>
Added this to the end of footer.phtml:
<?php echo $this->getChildHtml('mediaforge_footer'); ?>
The module definition is pretty basic and the two template files mediaforge_default.phtml and mediaforge_product.phtml are pretty simple so I won't bother including them.
Any ideas?
In a nutshell, you need to define a cache key for your block, which means you'll need to use something other than Mage_Core_Block_Template. When you create your own block, add this to the constructor:
protected function _construct() {
$this->addData(array(
'cache_lifetime' => 3600,
'cache_key' => $this->someMethodToDifferentiatePages(),
));
}
That last method needs to return a different string to every use case of the block (e.g. one for catalog pages, one for "other" if that's all you need). This will tell Magento which cached version to use
Hope that helps!
Thanks,
Joe

Resources