Adding an Image Attribute To A Product

This post deals with adding an image (or any other type of media) to a product. This is useful where an image is required to be separate from Magento’s built in product media system.

The Installer

First, we want to create our attribute.

<?php
$installer = $this;

$installer->startSetup();

$this->addAttribute('catalog_product', 'poster_frame', array(
    'attribute_set' => 'Default',
    'group'         => 'iWeb Media',
    'input'         => 'image',
    'type'          => 'varchar',
    'label'         => 'Poster Frame',
    'visible'       => true,
    'required'      => false,
    'visible_on_front' => true,
    'global'        => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_GLOBAL,
    'backend'       => 'iwebmedia/product_attribute_backend_image',
    'input_renderer'=> 'iwebmedia/adminhtml_product_image',
    'sort_order' => 10
));

$this->endSetup();
  • Because we’re saving an image, we need to create our own backend class to deal with the saving of the image and setting the filename of the image on the product object.
  • We also need to create a custom renderer for the input. This is a block class which we’ll create in our module. By default, Magento will use its own image renderer which is Mage_Adminhtml_Block_Catalog_Category_Helper_Image. This extends Varien_Data_Form_Element_Image and hardcodes the getUrl() method to look in media/catalog/product.

Note: The input_renderer is a special field for the catalog_product EAV type and is stored in the catalog_eav_attribute table as frontend_input_renderer.

The Input Renderer

<?php
class Iweb_Media_Block_Adminhtml_Product_Image extends Varien_Data_Form_Element_Image{
    protected function _getUrl()
    {
        $url = false;
        if ($this->getValue()) {
            $url = Mage::helper('iwebmedia')->getPlaceholderUrl() . $this->getValue();
        }
        return $url;
    }
}

– We just needed to change the getUrl() method to return the path to our image location, which we’ve got a helper to do so that’s only in one location.

The Helper Class

<?php
class Iweb_Media_Helper_Data extends Mage_Core_Helper_Abstract {
    const PLACEHOLDER_UPLOAD_DIR = "iwebmedia";
    
    public function getPlaceholderDir(){
        return Mage::getBaseDir('media') . DS . self::PLACEHOLDER_UPLOAD_DIR . DS;
    }
    
    public function getPlaceholderUrl(){
        return Mage::getBaseUrl('media') . '/' . self::PLACEHOLDER_UPLOAD_DIR . '/';
    }
}

The Attribute’s Backend Model

This is the class which deals with the saving of our data and image

<?php
class Iweb_Media_Model_Product_Attribute_Backend_Image extends Mage_Eav_Model_Entity_Attribute_Backend_Abstract{        
    public function beforeSave($object){
        parent::beforeSave($object);
        
        $name = $this->_getName();
        $imageData = $object->getData($name);
        
        if(isset($imageData['delete']) && (bool) $imageData['delete']){
            return $this->_removeImage($object, $imageData['value']);
        }else{
            return $this->_uploadImage($object);
        }
    }
    
    protected function _getHelper(){
        return Mage::helper('iwebmedia');
    }
    
    protected function _getName(){
        return $this->getAttribute()->getName();
    }
    
    protected function _removeImage($object, $fileName){
        $file = $this->_getHelper()->getPlaceholderDir() . $fileName;
        $name = $this->_getName();
        
        if(file_exists($file)){
            unlink($file);
        }
        
        $object->setData($name, '');
    }
    
    protected function _uploadImage($object){
        $name = $this->_getName();
         
        if(!isset($_FILES[$name]) || (int) $_FILES[$name]['size'] <= 0){
            return;
        }
        
        $path = $this->_getHelper()->getPlaceholderDir();
        
        $uploader = new Varien_File_Uploader($_FILES[$name]);
        $uploader->setAllowedExtensions(array('jpg','jpeg','gif','png'));
        // Allow Magento to create a name for this upload!
        $uploader->setAllowRenameFiles(true);
        
        $result = $uploader->save($path);
        
        $object->setData($name, $result['file']);
    }
    
}

– We use the beforeSave() method which then uses the delete input (provided by Magento’s Varien_Data_Form_Element_Image class) to check whether we should remove the existing image or upload a new one.
– The Varien_File_Uploader class deals with the validation of the image and will throw an error in case of any failure.
– setAllowRenameFiles(true) allows the file uploader to create a new file name if the current one already exists.
$object->setData($name, $result[‘file’]); sets the new filename on the product for saving.

Simplified Magento Flow with Events

The Mage_Core_Controller_Varien_Front is instantiated. This is the class which is always used despite its front name. It picks which routers should be used.

Event: controller_front_init_before
Event: controller_front_init_routers

Mage_Core_Controller_Varien_Front → dispatch() → Router Match Loop. The standard routers are:

  • Mage_Core_Controller_Varien_Router_Standard (Frontend)
  • Mage_Core_Controller_Varien_Router_Admin (Admin)
  • Mage_Core_Controller_Varien_Router_Default (Default)

When a router matches in the loop, its job is to set the following on the request:

$request->setModuleName($module);
$request->setControllerName($controller)
$request->setActionName($action)
$request->setControllerModule($realModule)
$request->setDispatched(true);

And then calls dispatch on the instance of the controller (Eg. Mage_Cms_IndexController).

Dispatch is defined in Mage_Core_Controller_Varien_Action, which is extended by Mage_Core_Controller_Front_Action, which should be extended by custom controllers on the frontend.

PreDispatch

The dispatch method calls predispatch in the controller. To not dispatch the controller, I.e to forward to another controller at this point, set:

$request->setDispatched(false);

And the controller’s action method will not be called. To switch controller, action, etc, change the values on the request object, and the router’s match loop will continue and attempt to dispatch the new controller. The predispatch method then fires:

Event: controller_action_predispatch
Event: controller_action_predispatch_ + getRouteName()
Event: controller_action_predispatch_ + getFullActionName()

These events can also be used to hook into, and forward to another controller if required. At this point, no blocks are created – the layout cannot be manipulated. If the request’s isDispatched method, remains as true, then the flow continues, and the controller’s action method is executed.

LoadLayout

The following two methods (loadLayout and renderLayout) must be called from the custom controller’s action method for these to apply

 $controller->loadLayout($handles)

is called with the option to supply handles. These replace the ‘default’ handle, so if using this, pass default in your handles array. Note: These handles will be added before any action layout handles, so the instructions within them may be overwritten.

addActionLayoutHandles()

adds the STORE_code layout handle, theme handle (THEME_*), and the full action name in lowercase (E.g. customer_account_index)

loadLayoutUpdates()

Event: controller_action_layout_load_before
This is the best event to use to add custom layout handles, however it will fire for every controller. See this for workarounds.

generateLayoutXml()

Event: controller_action_layout_generate_xml_before

generateLayoutBlocks()

Event: controller_action_layout_generate_blocks_before
The XML has been generated at this point, not any actual layout blocks. If required, modifications to the XML could be made at this point.

$layout->generateBlocks()

The blocks are now created. Each block fires:
Event: core_block_abstract_prepare_layout_before
before the block’s _prepareLayout() is called, followed by
*Event:* core_block_abstract_prepare_layout_after

Event: controller_action_layout_generate_blocks_after
This is where blocks can programatically be inserted into the layout.

RenderLayout

Event: controller_action_layout_render_before
Event: controller_action_layout_render_before_ + fullActionName
where blocks can also be added to the layout programatically, however a reference to the layout object is not passed in the dispatchd observer, so the layout must be retrieved using Mage::app()->getLayout()

PostDispatch

The controller’s postDispatch() method is then called, firing the following events:

Event: controller_action_postdispatch
Event: controller_action_postdispatch_ + $this->getFullActionName()
Event: controller_action_postdispatch_ + $this->getRouteName()

Adding Layout Handles Programatically (and in order)

Adding custom layout handles in the correct order can sometimes be an awkward process, it can often be tempting to use either the controller_action_predispatch or the controller_action_predispatch_fullActionName, event and then add the layout handle in the corresponding observer code.

The problem with this approach is that these events are fired in the controller’s preDispatch method, meaning loadLayout hasn’t yet been called and there won’t be any other layout handles present at that point. This causes problems because your layout handle may rely on items from other layout handles which haven’t been loaded at that point.

Correcting the handles’ order

One method to fix this issue is to use the controller_action_layout_load_before event which is fired in the loadLayoutUpdates, method (this is called via the loadLayout method). At this point, addActionLayoutHandles has already been executed, which makes relying on other handles a possibility. One problem with this approach is the observer method will be called for every page, which won’t work correctly for code which relies on running for a fullActionName. One fix for this method is to either check the existence of a required layout handle or controller action’s full name in the observer code.

XML

<controller_action_layout_load_before>
    <observers>
        <Demo_Module>
            <class>skin/observer</class>
            <method>updateLayoutHandle</method>
        </Demo_Module>
    </observers>
</controller_action_layout_load_before>

Observer

public function updateLayoutHandle($observer)
{
    $update = $observer->getEvent()->getLayout()->getUpdate();
    $handles = $update->getHandles();

    foreach($handles as $k => $handle){
        if($handle == 'catalog_category_default'){
            $handles[$k] = 'catalog_category_layered';
        }
    }

    $update->resetHandles();
    $update->addHandle($handles);   
}

This allows us to get all layout handles, change them as necessary, and re-add them (an array can be passed to the addHandle method). We could also splice in handles at required points using this method.