I have a custom module, which has several blocks. If I include these blocks in a CMS page after each other, they work as expected. If I include them through the layout XML files, they all display the source code of the last one called in the XML. A minimum test case (that for me is exhibiting this behavior) follows, along with expected and actual results.
Code
/app/etc/modules/Test_Tester.xml
<?xml version="1.0"?>
<config>
<modules>
<Test_Tester>
<active>true</active>
<codePool>local</codePool>
</Test_Tester>
</modules>
</config>
/app/code/local/Test/Tester/etc/config.xml
<?xml version="1.0"?>
<config>
<modules>
<Test_Tester>
<version>0.1.0</version>
</Test_Tester>
</modules>
<global>
<blocks>
<test_tester>
<class>Test_Tester_Block</class>
</test_tester>
</blocks>
</global>
</config>
/app/code/local/Test/Tester/Block/One.php
<?php
class Test_Tester_Block_One extends Mage_Catalog_Block_Product_List_Upsell
{
protected function _prepareData()
{
echo 'One.php';
//...MORE code here, it's not really relevant though
}
}
/app/code/local/Test/Tester/Block/Two.php
<?php
class Test_Tester_Block_Two extends Mage_Catalog_Block_Product_List_Upsell
{
protected function _prepareData()
{
echo 'Two.php';
//...MORE code here, it's not really relevant though
}
}
/app/design/frontend/INTERFACE/TEMPLATE/layout/page.xml (under
..
<block type="core/text_list" name="testa" as="testa" />
<block type="core/text_list" name="testb" as="testb" />
..
/app/design/frontend/INTERFACE/TEMPLATE/layout/cms.xml (under
<reference name="testa">
<block type="test_tester/one" template="tester/one.phtml"/>
</reference>
<reference name="testb">
<block type="test_tester/two" template="tester/two.phtml"/>
</reference>
/app/design/frontend/INTERFACE/TEMPLATE/template/page/home_template.phtml
<?php echo $this->getChildHtml('testa'); ?>
<?php echo $this->getChildHtml('testb'); ?>
/app/design/frontend/INTERFACE/TEMPLATE/tester/one.phtml
one.phtml
/app/design/frontend/INTERFACE/TEMPLATE/tester/two.phtml
two.phtml
Expected
This should print out (on the homepage, where the blocks are being included):
One.php
one.phtml
Two.php
two.phtml
Actual Output
If I include the blocks within the Homepage CMS page, like so:
{{block type="test_tester/one" template="tester/one.phtml"}}
{{block type="test_tester/two" template="tester/two.phtml"}}
...I get the expected output. However, using the layout as above in the code sample, I get:
Two.php
two.phtml
Two.php
two.phtml
I think I've gone insane - I can't see the bit I'm mucking up.
Try giving your blocks names in the homepage CMS page. Similar errors I've received have been resolved this way. I see that you're trying to wrap the blocks in text lists, but from what I see you never actually identify the blocks from within the CMS page. Try something to this effect instead:
{{block type="test_tester/one" template="tester/one.phtml" name="testa"}}
{{block type="test_tester/two" template="tester/two.phtml" name="testb"}}
If you cannot get away from the parent blocks as containers, you may have to rethink part of your layout. If that doesn't do it either way, let me know and we'll try something else. Hope that helps.
Thanks,
Joe
Related
I'd like to add a static box under the price on each product page, but I dont want to overwrite an existing template file (e.g. catalog/product/view.phtml) to render it's child block.
I tried to add a block element via frontend/base/default/layout/local.xml
<layout version="0.1.0">
<default>
<reference name="product.info">
<block type="telllowerpricelink/linkbox" name="telllowerpricelink.linkbox" template="telllowerpricelink/link.phtml" before="product.description" output="toHtml" />
</reference>
</default>
</layout>
Then i built a rudimental module:
app/code/local/MyPackage/TellLowerPriceLink/Block/LinkBox.php
<?php
class MyPackage_TellLowerPriceLink_Block_Link extends Mage_Core_Block_Template
{
}
?>
app/code/local/MyPackage/TellLowerPriceLink/etc/config.xml
<?xml version="1.0"?>
<config>
<modules>
<MyPackage_TellLowerPriceLink>
<version>0.1.0</version>
</MyPackage_TellLowerPriceLink>
</modules>
<global>
<blocks>
<mypackage_telllowerpricelink>
<class>MyPackage_TellLowerPriceLink_Block</class>
</mypackage_telllowerpricelink>
</blocks>
</global>
</config>
And my templatefile:
design/frontend/base/default/template/telllowerpricelink/link.phtml
<?php
echo 'Hello world!';
?>
So my questions are:
1. The main question: Is it possible to add the html-output (btw: I dont see it) at the end of the parent block element without editing its template (a la renderChildHtml)?
2. Is it possible to store my template file in this folder or have I to copy the hole default theme folder into a own theme?
Thanks a lot, I have googled and read a lot but didnt find a satisfying answer.
That's only possible with blocks derived from Mage_Core_Block_Text_List block so in your case it isn't possible but you could add your block to reference content (a container that renders all the children from layout files) and wrap your block into a div with style="display: none;" set and then move it with a javascript to the expected location.
It's possible but instead you could change the default theme in admin under system->configuration->general->design->package
The name that you will use here will be the name of your theme folder (note that this changes the theme for your entire store) so you can create your folder in desing/your_theme_name_from_admin/{layout, template} and put only files that you require to override in there. With this you could copy only the phtml file that you would like to override and change it on the new location while keeping the default copy in tact.
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.)
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
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
I am writing a module to extend the Core/Catalog/Product/View/Media.php class so I can expose new methods in my template. I followed a number of guides online, including Alan Storms excellent series, but have had little success. ( http://alanstorm.com/magento_config )
I posted my code on Github: https://github.com/razialx/Magento-Overwrite-Block
I am not getting any errors in the logs, it just isn't loading my class. Very perplexed.
One thought I had was that I may only be able to rewrite classes that are explicitly defined. I know the Mage_Catalog_Block_Product_View_Media class is never defined in a config file, though I assume it is referenced by the layout xml file catalog.xml
<block type="catalog/product_view_media" name="product.info.media" as="media" template="catalog/product/view/media.phtml"/>
Your config.xml is slightly off. Give the following a try.
<config>
<modules>
<Test_Catalog>
<version>1.0.0.0.0</version>
</Test_Catalog>
</modules>
<global>
<blocks>
<catalog>
<rewrite>
<product_view_media>Test_Catalog_Block_Product_View_Media</product_view_media>
</rewrite>
</catalog>
</blocks>
</global>
</config>
Your <blocks> node needs to be enclosed in a <global> node.