Adding Extra Attributes to All Product Collections

This can be very useful for attributes which need to be available on all product collections. This is to be used sparingly, however – adding a lot of attributes to a product collection can drastically impact performance; all attributes should also be indexed into the flat tables so that they’re available when flat catalog is on.

Config

<frontend>
	<events>
		<catalog_product_collection_load_before>
			<observers>
				<llapgoch_pcattributes_add>
					<model>llapgoch_addpcattributes/observer</model>
					<method>addAttributes</method>
					<type>model</type>
				</llapgoch_pcattributes_add>
			</observers>
		</catalog_product_collection_load_before>
	</events>
</frontend>

<default>
	<llapgoch_addpcattributes>
		<general>
			<attributes></attributes>
		</general>
	</llapgoch_addpcattributes>
</default>

– We add an observer to the catalog_product_collection_load_before event. This will make sure our attributes are added to the collection wherever it’s instantiated from.
– We also add a default node as a placeholder – we’ll allow loaded attributes to be set via the admin’s system configuration.

Observer

<?php
class Llapgoch_AddProductCollectionAttributes_Model_Observer{
	public function addAttributes($observer){
		$attrs = Mage::helper('llapgoch_addpcattributes')->getAttributesToAdd();
		
		if(is_array($attrs) && count($attrs)){
			$observer->getCollection()->addAttributeToSelect($attrs);
		}
	}
}

– All we need to do is get the attributes we’d like to add to the collection and add them to the select object.

Helper

<?php
class Llapgoch_AddProductCollectionAttributes_Helper_Data extends Mage_Core_Helper_Abstract{
	const XML_PATH_PRODUCT_ATTRIBUTES = "llapgoch_addpcattributes/general/attributes";
	
	public function getAttributesToAdd(){
		$attrs = explode(" ", Mage::getStoreConfig(self::XML_PATH_PRODUCT_ATTRIBUTES));
		
		if(count($attrs)){
			return $attrs;
		}
		
		return false;
	}
		
}

– We load the attributes which can either be set in the admin area (see System Configuration) or overridden by another module’s config.xml.
– We split the attributes into an array using spaces, but this could be any character.

System Configuration

<?xml version="1.0"?>
<config>
	<tabs>
		<llapgoch translate="label">
			<label>LLAP-Goch</label>
			<sort_order>100</sort_order>
		</llapgoch>
	</tabs>
	<sections>
		<llapgoch_addpcattributes translate="label" module="llapgoch_addpcattributes">
			<label>Product Attributes</label>
			<tab>llapgoch</tab>
			
			<sort_order>1</sort_order>
			<show_in_default>1</show_in_default>
			<show_in_website>1</show_in_website>
			<show_in_store>1</show_in_store>
			
			<groups>
				<general translate="label">
					<label>General</label>

					<sort_order>10</sort_order>
					<show_in_default>1</show_in_default>
					<show_in_website>1</show_in_website>
					<show_in_store>1</show_in_store>
					<fields>
						<attributes translate="label">
							<label>Collection Attributes To Add</label>
							<comment>Space separate attributes to be added to every product collection</comment>
							<frontend_type>text</frontend_type>
							<backend_model>llapgoch_addpcattributes/system_config_backend_attributestring</backend_model>
							<sort_order>1</sort_order>
							<show_in_default>1</show_in_default>
							<show_in_website>1</show_in_website>
							<show_in_store>1</show_in_store>
						</attributes>
					</fields>
				</general>
			</groups>
		</llapgoch_addpcattributes>
	</sections>
</config>

– We just add our field as a text area to the system config.
– We use a backend model which will be used to warn the user if commas are detected in the string because we’ve chosen to split attributes on spaces.

Backend Model

<?php
class Llapgoch_AddProductCollectionAttributes_Model_System_Config_Backend_Attributestring extends Mage_Core_Model_Config_Data{
	public function _afterSave(){
		$helper = Mage::helper('llapgoch_addpcattributes');
		
		if(strpos($this->getValue(), ",") !== false){
			 Mage::getSingleton('core/session')->addNotice($helper->__('Please use spaces to separate your attribute names instead of commas'));
		}
		
		parent::_afterSave();
		
	}
}

– Checks the string for the existence of spaces and adds a notice for the user if so.

This code is available as a complete module here: https://github.com/llapgoch/magento-add-attributes-to-product-collection

Emails in Magento: A Guide

Emails in Magento can be incredibly tricky. Firstly, you’ll want to define a field in system config for the template itself; the location of this field dictates the structure of the default node it uses to retrieve available templates and children.

<config>
    <tabs>
        <rayware translate="label" module="rayware">
            <label>Rayware</label>
        </rayware>
    </tabs>
    <sections>
        <rayware translate="label" module="rayware">
            <label>General</label>
            <tab>rayware</tab>
            <groups>
                <reg_email translate="label">
                    <label>Registration Email</label>
                    <fields>
                        <email_template translate="label description" module="rayware">
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                            <label>Email Template</label>
                            <source_model>adminhtml/system_config_source_email_template</source_model>
                            <sort_order>10</sort_order>
                            <frontend_type>select</frontend_type>
                            <description>The template used to send new user reqistrations to rayware</description>
                        </email_template>
                    </fields>
                </reg_email>
            </groups>
        </rayware>
    </sections>
</config>

The class adminhtml/system_config_source_email_template finds its default using its own node structure by converting its slashes to underscores, and then looking in global/defaults/template/email so it’ll look here:

<global>
    <template>
        <email>
            <rayware_reg_email_email_template translate="label" module="rayware">
                <label>[Rayware] New Customer Email</label>
                <file>rayware/new-customer.phtml</file>
                <type>html</type>
            </rayware_reg_email_email_template>
        </email>
    </template>
</global>

So: The location of system config setting for the email template dictates where the actual template should live in global/template/email node.

Defaults

The structure of the system config node above dictates where the default should be located. In this instance, it’ll be the following – and the contents of the node should refer to the node name in global/template/email/ above

<default>
	<rayware>
		<reg_email>
			<email_template>rayware_reg_email_email_template</email_template>
		</reg_email>
	</rayware>
</default>

Sending Emails

$mailTemplate = Mage::getModel('core/email_template');
$mailTemplate->setDesignConfig(array(
	'area' => 'frontend',
	 'store' => Mage::app()->getStore()->getId()
));

$mailTemplate->sendTransactional($templateId, $senderName, $toEmail, $toName, $bind = array(), $storeId = null);
  • Templateid can either be a string or a number, if it’s retrieved from the config then it’ll be worked out automatically if the value has been saved. For the current example, Mage::getStoreConfig(‘rayware/reg_email/email_template’); would be used
  • Sender Name – this refers to one of the transactional sender names in the admin. The name entered here is used to look up a config path in Mage::getStoreConfig(‘trans_email/ident_’ . $sender . ‘/name’);
  • toEmail – the email to send to, this can be an array of email addresses
  • toName – the name of the person to send the email to – this can be an array of names
    bind – array of variables which can be used in the email (See directives)

Templates

Define what variables are available in the email using comments at the top of each email file. The subject is also defined in this way.

logo_url and logo_alt are populated from the admin area. The purpose of variables is to allow easy inserting of them from the admin area – this would appear to be their only purpose. The part after the colon is the part which will be shown in the dropdown in the admin (the user friendly name).

<!--@subject Your Quote #{{htmlescape var=$quotation.quote_id}}@-->
<!--@vars
{"store url=\"\"":"Store Url",
"var logo_url":"Email Logo Image Url",
"var logo_alt":"Email Logo Image Alt"}
@-->

Email template files live in app/locale/<<country_CODE>>/template/email/ where country code is the code of the country set in the admin area.
The templates can also be placed in en_US, which is the last level of fallback.

Email templates in the database live in the core_email_template table.

Any of the vars can be placed in the subject field using the above, E.g:

<!--@subject {{var title}}@-->

Directives

Var

Used to pull out any variables which are passed in to the template via the ‘bind’ parameter. Methods can also be called on passed in variables on the email objects:

{{var subscriber.getConfirmationLink()}}
{{var passedInVariable.horse}}
$bind = array(
    'passedInVariable'  => array(
        'horse' => ‘Moose’
    }
);

Block

Creates the block, and sets the parameters on them (or calls the methods). If type is not set as a parameter

{{block type=‘cms/block’ customVar=‘1’ methodToCall=‘test’}} 

Protocol

{{protocol}} -current protocol http or https
{{protocol url="www.domain.com/"}} domain URL with current protocol
{{protocol http="http://url" https="https://url"} - also allow additional parameter "store"

Config

{{config path="path/to/config}}

CustomVar

{{customvar code="custom_var_code"}}

Inlinecss

{{inlinecss file="path/to/file.css"}}

Layout

Loads the layout handle specified and sets all variables on each of the blocks in the layout handle. Methods can also be called on the blocks in the layout handle using this method, if they exist. “area” can also be set as a parameter but will default to the current area if not. Most likely this will be ‘frontend’.

{{layout handle="my_custom_layout_handle_to_be_used" variable="test" method="moose"}}

Skin

Embeds the url to a skin asset.

{{skin url="path/to/asset.ext"}}

Media

Embeds a media url.

{{media url="path/to/media/asset.ext"}}

Store

Get the URL for the store. If the url doesn’t end in a suffix, then ‘URL’ can be used, otherwise, direct_url should be used instead. Parameters can also be passed into the url using the syntax _query_paramName=”paramValue”.

{{store url="contacts"}} // http://dave.magento.store/contacts/
{{store direct_url="html_file.html"}} // http://dave.magento.store/html_file.html
{{store direct_url="goosey.html" _query_param="horseparam"}} // http://dave.magento-test-2.iweb/goosey.html?param=horseparam

htmlescape

Escapes the variable which is passed in from the bind array. certain tags can also be ignored using the allowed_tags parameter

{{htmlescape var="<b>Moose</b>" allowed_tags="b"}} // This would output the moose and keep the bold tags unescaped. See the var directive above for referencing bind params in the same way for htmlescape.

Caching

Retrieving the cache object

<?php Mage::app()->getCache();

Checking if a Cache Type is Enabled

<?php
Mage::app()->useCache($cache_type);

Saving to and Retrieving from Cache

Setting $lifetime to 0 or null means the value will be cached indefinitely, alternately enter a second value.

<?php
$cache->save($value, $key, $tags = array(), $lifeTime=null);
$cache->load($key);
$cache->remove($key);

Clearing a Cache using Tags

<?php $cache->clean($tags = array());

Clearing Cache from the Admin

To clear the custom caches from the admin, use the “Flush Cache Storage” button, as the “Flush Magento Cache” button only deals with Magento’s own cache. Obviously, this is if a custom cache type hasn’t been added to the admin using the method above.

For your custom cache type to appear in the admin, use the following XML to your module’s config file:

<global>
    <cache>
        <types>
             <your_cache_type module="your_module" translate="label description">
                <label>Your Custom Cache Label</label>
                <description>Description of cache type.</description>
                <tags>YOUR_CACHE_TAG</tags>
             </your_cache_type>
        </types>
    </cache>
</global>

Block Caching

To change the caching methods on a block, you can change the cache key to make the block cache for a specific thing. Eg, for a category key:

<?php $block->setCacheKey($category_key);

Or, in XML:

<block type="custom/block" name="custom.block">
    <action method="setCacheKey">
        <key>my_key</key>
        <!-- or to make it dynamic... -->
        <key helper="myhelper/getCacheKey" />
    </action>
</block>

Turning off Caching for a Block

$block->setCacheLifetime(null);

In XML:

<action method=”setCacheLifetime” />

Making a Block Cache Permenantly

<?php $block->setCacheLifetime(false);

Magento’s Topmenu

Very few blocks cache by default in Magento, however the Topmenu block is set up to (presumably because of the possibly heavy queries in generating them)

Caching should be set in a block’s constructor if it’s not being set in XML. E.g, The Mage_Page_Block_Html_Topmenu‘s constructor looks like the following:

<?php
public function _construct()
{
    $this->addData(array(
        'cache_lifetime' => false,
    ));
}

This behaves the same as $this->setCacheLifetime(false), meaning it will cache indefinitely. A problem with the top menu is that it caches regardless of whether the page is secure or not. If a secure page loads an unsecured cache then any links to elements will cause browser warnings; to get around this, we can do something like the following in the constructor which will create a separate cache of the block for http and https:

<?php
$this->addData('cache_key', Mage::app()->getStore()->isCurrentlySecure() ? "secure" : "unsecure");

**TODO: getCacheKeyInfo **

Display Shipping Methods For A Product

This will display appropriate shipping methods on the product detail page

The Block Class

class Namespace_Module_Block_Catalog_Product_Delivery extends Mage_Core_Block_Template{

    protected $_product;

    public function _construct(){
       parent::_construct();
       $this->setTemplate('catalog/product/delivery.phtml');
    }

    public function setProductId($productId){
        $this->_product = Mage::getModel('catalog/product')->load($productId);
    }

    public function getDeliveryMethods(){
        $quote = Mage::getModel('sales/quote');
        $existingQuote = Mage::getSingleton('checkout/session')->getQuote();

        if($countryId = $existingQuote->getShippingAddress()->getCountryId()){
            $quote->getShippingAddress()->setCountryId($countryId);
        }else{
            $quote->getShippingAddress()->setCountryId(Mage::getStoreconfig('general/country/default'));
        }

        $quote->addProduct($this->_product);
        $quote->getShippingAddress()->collectTotals();
        $quote->getShippingAddress()->setCollectShippingRates(true);
        $quote->getShippingAddress()->collectShippingRates();
        $rates = $quote->getShippingAddress()->getShippingRatesCollection();

       return $rates;
    }

}
  • Load a new quote object and populate its address with the country of the existing quote or the default config country.
  • Add the product to the quote
  • return the rates

The Template

<?php
$_rates = $this->getDeliveryMethods();
?>

<p><?php echo $this->__("DELIVERY OPTIONS");?></p>
<?php if(count($_rates)):?>
    <ul class="product-rates">
    <?php foreach($_rates as $_rate):?>
        <li><?php echo $this->escapeHtml($_rate->getMethodTitle());?></li>
    <?php endforeach; ?>
    </ul>
<?php endif;?>