Tuesday, January 29, 2013

Share cart between websites

I saw that this is a general issue among Magneto users/developers.
Here is how I was able to keep the cart between websites.
The solution is not fully tested yet but for me it seams to work.
[Update]
Warning: I seams that this solution doesn't work with multiple currencies installed. See comments from MichaƂ Burda
[/Update]
If someone tries it please let me know the result and eventual bugs that appear.
Preconditions:
  • 'Use SID on Frontend' must be st to 'Yes' (System->Configuration->Web->Session Validation Settings)
Approach:
Magneto already allows you to keep the quote (cart) between store views from the same website.
I tried to manipulate this feature into letting me keep the cart between all websites (store views).
How is the quote kept in $_SESSION?
Magento keeps the quote id in the $_SESSION like this:
$_SESSION['quote_id_5'] = 34;
In the code above, 34 represents the quote id and 5 represents the website it (not store view id).
So it's basically like this:
$_SESSION['quote_id_{WEBSITE_ID}'] = {QUOTE_ID};
This means that the quote is different for each website.
Now for the actual code:
I've created a new extension called Easylife_Sales.
The extension has a fail-safe, in case it doesn't work as expected, it's behavior can be disabled from the configuration area. but more on this later.
Extension files:
app/etc/modules/Easylife_Sales.xml - declaration file
<?xml version="1.0"?>
<config>
    <modules>
        <Easylife_Sales>
            <active>true</active>
            <codePool>local</codePool>
            <depends>
             <Mage_Sales /><!-- so it's loaded after Mage_Sales -->
             <Mage_Checkout /><!-- so it's loaded after Mage_Checkout -->
            </depends>
        </Easylife_Sales>
    </modules>
</config>
app/code/local/Easylife/Sales/etc/config.xml - configuration file
<?xml version="1.0"?>
<config>
 <modules>
  <Easylife_Sales>
   <version>0.0.1</version>
  </Easylife_Sales>
 </modules>
 <global>
  <models>
   <sales>
    <rewrite>
     <observer>Easylife_Sales_Model_Observer</observer>
     <quote>Easylife_Sales_Model_Quote</quote>
    </rewrite>
   </sales>
   <checkout>
    <rewrite>
     <session>Easylife_Sales_Model_Checkout_Session</session>
    </rewrite>
   </checkout>
  </models>
  <helpers>
   <sales>
    <rewrite>
     <data>Easylife_Sales_Helper_Data</data>
    </rewrite>
   </sales>
  </helpers>
 </global>
 <frontend>
  <events><!-- this section is not mandatory, explanations later -->
   <sales_quote_collect_totals_before>
    <observers>
     <easylife_sales>
      <class>sales/observer</class>
      <method>checkProductAvailability</method>
     </easylife_sales>
    </observers>
   </sales_quote_collect_totals_before>
  </events>
 </frontend>
 <default>
  <checkout>
   <options>
    <persistent_quote>1</persistent_quote><!-- this is for the fail-safe -->
   </options>
  </checkout>
 </default>
</config>
app/code/local/Easylife/Sales/Helper/Data.php - override the default sales helper to add the fail-save method
<?php 
class Easylife_Sales_Helper_Data extends Mage_Sales_Helper_Data{
    public function getIsQuotePersistent(){
  return Mage::getStoreConfigFlag('checkout/options/persistent_quote');
 }
}
app/code/local/Easylife/Sales/Model/Checkout/Session.php - override the default checkout session in order to change the session key for the quote
<?php 
class Easylife_Sales_Model_Checkout_Session extends Mage_Checkout_Model_Session{
 protected function _getQuoteIdKey()
    {
     if (Mage::helper('sales')->getIsQuotePersistent()){//if behavior is not disabled
         return 'quote_id';
     }
     return parent::_getQuoteIdKey();
    }
}
app/code/local/Easylife/Sales/Model/Quote.php - override the quote model to share between all websites
<?php 
class Easylife_Sales_Model_Quote extends Mage_Sales_Model_Quote{
 public function getSharedStoreIds(){
  if (Mage::helper('sales')->getIsQuotePersistent()){//if behavior is not diasabled
   $ids = Mage::getModel('core/store')->getCollection()->getAllIds();
   unset($ids[0]);//remove admin just in case
   return $ids;
  }
  return parent::getSharedStoreIds();
 }
}
app/code/local/Easylife/Sales/etc/system.xml - this allows you do disable the functionality in case something is wrong.
<?xml version="1.0"?>
<config>
 <sections>
  <checkout>
   <groups>
    <options>
     <fields>
      <persistent_quote translate="label" module="sales">
       <label>Keep cart between websites</label>
       <frontend_type>select</frontend_type>
       <source_model>adminhtml/system_config_source_yesno</source_model>
       <sort_order>100</sort_order>
       <show_in_default>1</show_in_default>
       <show_in_website>0</show_in_website>
       <show_in_store>0</show_in_store>
      </persistent_quote>
     </fields>
    </options>
   </groups>
  </checkout>
 </sections>
</config>
We are almost done. At this point the following happens. If you have a product in cart, you change the website and the product is not available in the new site the product is still in the cart. If you want this behavior then there is no problem. All you need to do is to remove these lines from config.xml
<observer>Easylife_Sales_Model_Observer</observer>
and
<events><!-- this section is not mandatory, explanations later -->
 <sales_quote_collect_totals_before>
  <observers>
   <easylife_sales>
    <class>sales/observer</class>
    <method>checkProductAvailability</method>
   </easylife_sales>
  </observers>
 </sales_quote_collect_totals_before>
</events>
In my case I wanted to remove from cart (permanently) the products that are not available in that website. if you want this behavior add the following file: app/code/local/Easylife/Sales/Model/Observer.php - this will remove from cart the products that are not valid on the current store.
<?php 
class Easylife_Sales_Model_Observer extends Mage_Sales_Model_Observer{
        public function checkProductAvailability($observer){
  if (!Mage::helper('sales')->getIsQuotePersistent()){
   return $this;
  }
  $quote = $observer->getEvent()->getQuote();
  $currentId = Mage::app()->getWebsite()->getId();
  
  $messages = array();
  
  foreach ($quote->getAllItems() as $item){   
   $product = $item->getProduct();
   if (!in_array($currentId, $product->getWebsiteIds())){
    $quote->removeItem($item->getId());
    $messages[] = Mage::helper('catalog')->__('Product %s is not available on website %s', $item->getName(), Mage::app()->getWebsite()->getName());
   }
  }
  foreach ($messages as $message){
   Mage::getSingleton('checkout/session')->addError($message);
  }
  return $this;
 }
}
Well that's about it. Enjoy and let me know how it turns out.

Marius.

Saturday, December 1, 2012

Ultimate module creator available on Magento connect

The Ultimate Module Creator is now available on Magneto connect: http://www.magentocommerce.com/magento-connect/catalog/product/view/id/15449/s/ultimate-modulecreator-8949/

You can still follow closely it's development here: https://github.com/tzyganu/moduleCreator

A few details about it: http://marius-strajeru.blogspot.ro/p/ultimate-module-creator.html#v1-0-0

Can someone please send me a design for the 'extension logo'? I'm reaaaaaly lousy at Photoshop.
Thanks in advance.
Marius.

Friday, November 2, 2012

Product attribute with custom options

Magento gives you the possibility to create dropdown product attributes. The problem I have sometimes is that the options for all the dropdown attributes are all in one single table. So for each option I get a (somehow) random ID. There are cases that I really need to know the id of one specific option and I don't want to hard code it. The solution for this is to have a custom source model for your dropdown attributes. The following example creates an attribute called 'Provider' with custom options. For the purpose of the example I'm going to use the namespace 'Easylife' and my extension is going to be names 'Provider'.
Here are the files you need to create:
app/code/local/Easylife/Provider/etc/config.xml - the extension config file
<?xml version="1.0"?>
<config>
    <modules>
        <Easylife_Provider>
            <version>0.0.1</version>
        </Easylife_Provider>
    </modules>
    <global>
        <models>
            <provider>
                <class>Easylife_Provider_Model</class>
            </provider>
        </models>
        <resources>
            <easylife_provider_setup>
                <setup>
                    <module>Easylife_Provider</module>
                    <class>Mage_Catalog_Model_Resource_Eav_Mysql4_Setup</class>
                </setup>
                <connection>
                    <use>core_setup</use>
                </connection>
            </easylife_provider_setup>
        </resources>
    </global>
</config>
app/code/local/Easylife/Provider/sql/easylife_provider_setup/mysql4-install-0.0.1.php - this will add an attribute named 'Provider' to your product
<?php
$this->startSetup();
//if you want to add the attribute to the category instead of the product change on the line below 'catalog_product' to 'catalog_category'
$this->addAttribute('catalog_product', 'provider', array(
        'group'                => 'General',
        'type'              => 'int',
        'backend'           => '',
        'frontend_input'    => '',
        'frontend'          => '',
        'label'             => 'Provider',
        'input'             => 'select',
        'class'             => '',
        'source'            => 'provider/attribute_source_provider',
        'global'             => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_GLOBAL,//can be SCOPE_WEBSITE or SCOPE_STORE
        'visible'           => true,
        'used_in_product_listing' =>true,//can also be false
        'frontend_class'     => '',
        'required'          => false,//can be true
        'user_defined'      => true,
        'default'           => '',
        'searchable'        => false,//can be true
        'filterable'        => false,//can be true
        'comparable'        => false,//can be true
        'visible_on_front'  => false,//can be true
        'unique'            => false,
        'position'            => 60,//put any number here
    ));
$this->endSetup();
app/code/local/Easylife/Provider/Model/Attribute/Source/Provider.php - this is the source of you attribute
<?php
class Easylife_Provider_Model_Attribute_Source_Provider extends Mage_Eav_Model_Entity_Attribute_Source_Abstract{
    protected $_options = null;
    public function getAllOptions($withEmpty = false){
        if (is_null($this->_options)){
            $this->_options = array();
                       //$this->_options[] = array('label'=>'HERE GOES THE LABEL', 'value'=>'HERE GOES THE VALUE');
            //as example
            $this->_options[] = array('label'=> $this->__('Provider 1'), value=>1);
            $this->_options[] = array('label'=> $this->__('Provider 2'), value=>2);
            $this->_options[] = array('label'=> $this->__('Provider 3'), value=>3);
        }
        $options = $this->_options;
        if ($withEmpty) {
            array_unshift($options, array('value'=>'', 'label'=>''));
        }
        return $options;
    }
    public function getOptionText($value)
    {
        $options = $this->getAllOptions(false);

        foreach ($options as $item) {
            if ($item['value'] == $value) {
                return $item['label'];
            }
        }
        return false;
    }
    public function getFlatColums()
    {
        $attributeCode = $this->getAttribute()->getAttributeCode();
        $column = array(
            'unsigned'  => false,
            'default'   => null,
            'extra'     => null
        );

        if (Mage::helper('core')->useDbCompatibleMode()) {
            $column['type']     = 'int(10)';
            $column['is_null']  = true;
        } else {
            $column['type']     = Varien_Db_Ddl_Table::TYPE_SMALLINT;
            $column['length']   = 10;
            $column['nullable'] = true;
            $column['comment']  = $attributeCode . ' column';
        }

        return array($attributeCode => $column);
    }
    public function getFlatUpdateSelect($store)
    {
        return Mage::getResourceModel('eav/entity_attribute')
            ->getFlatUpdateSelect($this->getAttribute(), $store);
    }
}
app/etc/modules/Easylife_Provider.xml - the module declaration
<?xml version="1.0"?>
<config>
    <modules>
        <Easylife_Provider>
            <active>true</active>
            <codePool>local</codePool>
            <depends>
                <Mage_Catalog />
            </depends>
        </Easylife_Provider>
    </modules>
</config>
That's it. Clear the cache and try again. If you choose to include the attribute in the product listing or if you made it filtrable
'used_in_product_listing' =>true,
...
'filterable'        => false
then you need to rebuild your indexes. Now if you need to get all the products that belong to 'Provider 1' all you need to do is this:
$products = Mage::getModel('catalog/product')->getCollection()->addAttributeToFilter('provider', 1);
Enjoy, Marius.

Tuesday, October 30, 2012

Add a new field to the store view

Here is the clear way to add a new field on the store view in Magento.
In order to do this you should create a new extension.
For the purpose of this demo I will use the 'namespace' Easylife. replace it with your own namespace if you want.
The extension is named Core because I'm overriding something in the core. You can name it how ever you want. Here are the files that need to be created:

app/code/local/Easylife/Core/etc/config:
<?xml version="1.0"?>
<config>
    <modules>
        <Easylife_Core>
            <version>0.0.1</version>
        </Easylife_Core>
    </modules>
    <global>
        <resources>
            <easylife_core_setup>
                <setup>
                    <module>Easylife_Core</module>
                </setup>
            </easylife_core_setup>
        </resources>
        <blocks>
            <adminhtml>
                <rewrite>
                    <system_store_edit_form>Easylife_Core_Block_Adminhtml_System_Store_Edit_Form</system_store_edit_form>
                </rewrite>
            </adminhtml>
        </blocks>
    </global>
</config>
app/code/local/Easylife/Core/sql/easylife_core_setup/mysql4-install-0.0.1.php
<?php 
$this->startSetup();
$this->run("ALTER TABLE `{$this->getTable('core/store')}` ADD COLUMN `custom` VARCHAR(255)");//change the name and type of the column if you need.
$this->endSetup();
app/code/local/Easylife/Core/Block/Adminhtml/System/Store/Edit/Form.php - this overrides the admin block.
<?php 
class Easylife_Core_Block_Adminhtml_System_Store_Edit_Form extends Mage_Adminhtml_Block_System_Store_Edit_Form{
    protected function _prepareForm(){
        parent::_prepareForm();
        if (Mage::registry('store_type') == 'store'){
            $storeModel = Mage::registry('store_data');
            $fieldset = $this->getForm()->getElement('store_fieldset');
            $fieldset->addField('custom', 'text', array(
                    'name'      => 'store[custom]',
                    'label'     => Mage::helper('core')->__('Custom'),
                    'required'  => true,//or false
                    'value'        => $storeModel->getData('custom') 
                ));
        }
        return $this;
    }
}
In order to activate your module you need this file
app/etc/modules/Easylife_Core.xml
<?xml version="1.0"?>
<config>
    <modules>
        <Easylife_Core>
            <active>true</active>
            <codePool>local</codePool>
            <depends>
                <Mage_Core/>
            </depends>
        </Easylife_Core>
    </modules>
</config>
Enjoy.
Marius.

Friday, September 28, 2012

Module creator

After a few months of development, not because of the work volume, but because of the lack of time, I finally did it.
I have created my own module creator.
For an (almost) complete description see this: Ultimate Module creator Let me know what you think.

Marius.

Monday, August 27, 2012

How to make CMS pages available only to logged in customers

Hello
Here is a possible solution on how to make cms mages available only to logged in customers.
Override the Mage_Cms_PageController. Here is a how to do it: http://www.extensionprogrammer.com/skin/frontend/default/dermodpro/pdf/magento-cheatsheet.pdf

Now add the following method in your new controller

public function preDispatch(){
		$restrictedIdentifiers = array('about-magento-demo-store');
		$pageId = $this->getRequest()
            ->getParam('page_id', $this->getRequest()->getParam('id', false));
        $page = Mage::getModel('cms/page')->load($pageId);
    	parent::preDispatch();
    	if (in_array($page->getIdentifier(), $restrictedIdentifiers)){
	    	if (!Mage::getSingleton('customer/session')->authenticate($this)) {
	            $this->setFlag('', 'no-dispatch', true);
	        }
    	}
    }
That's it. Enjoy. I know it's not 100% clean solution. If you want the clean solution, add a new field for the cms_page table (let's call it 'password_protect'), add the field in the admin form. After doing this the code above becomes:
public function preDispatch(){
		$pageId = $this->getRequest()
            ->getParam('page_id', $this->getRequest()->getParam('id', false));
        $page = Mage::getModel('cms/page')->load($pageId);
    	parent::preDispatch();
    	if ($page->getPasswordProtect()){
	    	if (!Mage::getSingleton('customer/session')->authenticate($this)) {
	            $this->setFlag('', 'no-dispatch', true);
	        }
    	}
    }
Marius.

Monday, August 13, 2012

Tips & tricks to speed up Magento

I've found on LikedIn a comment about tips and tricks for speeding up Mangeto.
It seams like a good list to consider when having troubles with the site speed.
The list was provided by Pieter Pabst.
Here it goes:


- Use KeepAlive in Apache.
- Make sure the query buffer on your MySQL instance is large enough.
- Hosting MySQL on another (V)Machine will help
- Migrate .htaccess to you apache config and turn off AllowOverride.
- Use some kind of opcode cashing, like APC of even better, Zend Server
- Use mod_deflate
- Use mod_expires
- Put var/cache in a /dev/ramdisk of tmpfs
- Use the CDN option, or at least something that is accessible via another DNS name (like content.yoursite of static.yoursite) as browsers are limited to X connections to a certain host at one time (use the URLs in System -> Configuration -> Web to accomplish this), but be aware of the fall-back mechanism for skins. Also beware that the upload script in the backend does not like it when it's hosted on another hostname (potential XSS). Your uploads will fail. To overcome this, make sure the backend uses local JS and the frontend uses CDN. To accomplish this use the configuration scope in the Web config section (default to local, storeviews to CDN).
- Move /media to another server
- After putting var/cache in ramdisk, put the rest somewhere with fast I/O. Like SAS disks or a SSD. Don't put it on something like iSCSI or SATA. Put media on cheap I/O like SATA, preferably on another server, or something assebeble via another hostname.
- If you have the option, don't use apache at all. Try ngnix.

Magento:
- Use flat catalog (System -> Configuration -> Catalog -> Frontend)
- Enable cache (System -> Cache management)
- Compile Magento when opcode caching is no option (System -> Tools -> - Compiling)

Hacking
- Disabling the fall-back mechanism for themes / skins and merging the base and your current into one will save you a lot of IO
- Try to code your modules in a way they can use cache
- Try full page caching, all tough you will have to buy a license for products like Zend Server, this will boost performance.
- Try cashing frequently used queries (in a request kind of context I mean) in f.e.a arrays. But beware of the memmory limit. Unset things of necessary.

Webdesign
- Use sprites
- Combine everything in one CSS file or use one of the extensions already mentioned.
- The same goes for javascript
- Use robots.txt to stop robots from crawling certain URLs with producs they probably already indexed trough other URLs. It will just cause additional load.

After you've done all that, you site needs to be immense popular to require load balancing of MySQL clustering.

Friday, April 20, 2012

Retrieve bestsellers

Here is how you can retrieve a list of bestsellers
$collection = Mage::getResourceModel('sales/report_bestsellers_collection')
            ->setModel('catalog/product')
            ->addStoreFilter(Mage::app()->getStore()->getId())//if you want the bestsellers for a specific store view. if you want global values remove this
            ->setPageSize(5)//se there the number of products you want returned
            ->setCurPage(1);
foreach ($collection as $_product){
    $realProduct = Mage::getModel('catalog/product')->load($_product->getProductId());
    //do something with $realProduct;
}
Make sure you refresehd the statistic for bestsellers. Reports->Refresh statistics. Select refresh lifetime statistics for bestsellers. Enjoy.