Writing a custom module in Magento

Posted on March 18th, 2011 in Magento, Web Development | 8 Comments »

There are a number of articles on the internet that explain how to write custom modules in magento but for a new developer that has never worked with Zend and has little experience coding for Magento even the simplest task can be daunting. In this article I will go over building a simple twitter module with database and session caching that is configurable via the administrative panel.

Twitters API Call

http://twitter.com/statuses/user_timeline/USERNAME.json?count=NUMBER_OF_TWEETS_TO_RETURN

This is the call to the Twitter API that retrieves the last tweets made by the specified user. Don’t ever try to throw this in one of your view templates making a call to the Twitter API every page you visit as you will only get blocked by Twitter. Read number three on the API Terms of Service. The module that I will develop here caches the result of the API call in the database and the users session. If another user visits the site before the database cache times out, the user will fetch the result from the database instead of making an unnecessary API call. Once a user fetches the result, any subsequent page that they visit will grab the result from the session until it expires. Both timeouts will be set in the admin panel of the module.

Codepools and telling magento about your new module

Magento has three codepools: core, community and local. The core codepool is dedicated for all of magentos system modules, the community codepool is for modules developed by magentos partners and the local codepool is for you as the developer to write custom code. Lets set up a new package and module for our new twitter module. Since I wrote this for my company (Tech And House) my package will be TechAndHouse and the module will be Twitter. (This module is not used on our site, just something I was playing with to document magento module creation). Set up the following folder structure under /app/code/local/:

Next, declare your shell module and its codepool. Create a file named TechAndHouse_Tweet.xml under /app/etc/modules/ with the following:

<?xml version="1.0"?>
<config>
    <modules>
        <TechAndHouse_Tweet>
            <active>true</active>
            <codePool>local</codePool>
        </TechAndHouse_Tweet>
    </modules>
</config>

SQL setup

Now that magento knows about our module, lets set up the database table that will cache the API call. Create config.xml under /app/code/local/TechAndHouse/Tweet/etc/ and define the modules setup handler:

<?xml version="1.0"?>
<config>
    <modules>
        <TechAndHouse_Tweet>
          <version>0.1.0</version>
        </TechAndHouse_Tweet>
    </modules>
 
    <global>
      <resources>
 
        <tweet_setup>
          <setup>
            <module>TechAndHouse_Tweet</module>
          </setup>
          <connection>
            <use>core_setup</use>
          </connection>
        </tweet_setup>
 
        <tweet_write>
          <connection>
            <use>core_write</use>
          </connection>
        </tweet_write>
 
        <tweet_read>
          <connection>
            <use>core_read</use>
          </connection>
        </tweet_read>
 
      </resources>
    </global>
</config>

Create install script, mysql4-install-0.1.0.php to create table th_tweet in the following directory: /app/code/local/TechAndHouse/Tweet/sql/tweet_setup/

<?php
  $installer = $this;
  $installer->startSetup();
 
  $installer->run("
    DROP TABLE IF EXISTS {$this->getTable('th_tweet')};
 
    CREATE TABLE {$this->getTable('th_tweet')} (
      `tweet_id` int(11) NOT NULL AUTO_INCREMENT,
      `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      `twitter_id` bigint(20) NOT NULL,
      `text` text NOT NULL,
      PRIMARY KEY (`tweet_id`)
    ) ENGINE=MyISAM  DEFAULT CHARSET=latin1
  ");
 
  $installer->endSetup();

With this code in place, visit any page on your store and the table will be created. What happens is that upon instantiation, magento reads the /app/etc/modules directory to get information about all its active modules. Then the config.xml file of each module is read. If a SQL setup handler is found then the appropriate version number is read after comparing to the core_resource table. If the core_resource table does not have a SQL handler listed, magento runs the installer. If the version number in the config file is greater than the version number in the core_resource table, magento runs the proper updater SQL file. For example, if the version number in the config file is 0.1.0 and in the config file the version number is 0.2.0, then magento will run the mysql4-upgrade-0.1.0-0.2.0.php.

Administration Section And ACL

Create a new file, system.xml in /app/code/local/TechAndHouse/Tweet/etc/ with the following contents:

<?xml version="1.0"?>
<config>
    <tabs>
        <techandhouse translate="label">
            <label>Tech And House Modules</label>
            <sort_order>100</sort_order>
        </techandhouse>
    </tabs> 
 
    <sections>
        <techandhouse translate="label">
            <label>Twitter</label>
            <tab>techandhouse</tab>
            <frontend_type>text</frontend_type>
            <sort_order>1000</sort_order>
            <show_in_default>1</show_in_default>
            <show_in_website>1</show_in_website>
            <show_in_store>1</show_in_store>
        </techandhouse>
    </sections>
</config>

With this little snippet we have added a Tech And House tab with a sub section of Twitter. Doesn’t really do much, and when we click on the Twitter section we get an Access Denied error. This happens because our Adminhtml application can not find an entry for our new section in the ACL. Go back to the modules config.xml file and add the following (dots represent areas that we are not touching):

<?xml version="1.0"?>
<config>
    <modules>
        ...
    </modules>
 
    <adminhtml>
        <acl>
            <resources>
                <admin>
                    <children>
                        <system>
                            <children>
                                <config>
                                    <children>
                                        <techandhouse>
                                            <title>Tech And House Module</title>
                                        </techandhouse>
                                    </children>
                                </config>
                            </children>
                        </system>
                    </children>
                </admin>
            </resources>
        </acl>
    </adminhtml>
 
    <global>
        ...
    </global>
</config>

After adding this, log out of the magento admin and log in again. Just clearing the cache will not work in this case. After you log in you will be able to click on the new module without the Access Denied error and there will be a new section in the ACL. See screenshots below:

Administration Fields

This administration panel needs some fields now. Adding options like a Yes/No dropdown and text fields can just be declared, like this:

In your system.xml file add the following under sections -> groups:

<?xml version="1.0"?>
<config>
    <tabs>
        ...
    </tabs> 
 
    <sections>
        <techandhouse translate="label">
            ...
            <groups>
                <general translate="label">
                    <label>General Options</label>
          	    <frontend_type>text</frontend_type>
          	    <sort_order>60</sort_order>
          	    <show_in_default>1</show_in_default>
          	    <show_in_website>1</show_in_website>
          	    <show_in_store>1</show_in_store>
                    <fields>
                        <enabled translate="label">
                            <label>Enable Twitter Module</label>
                            <frontend_type>select</frontend_type>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                            <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>
                        </enabled>
                        <username translate="label">
                            <label>Twitter Username</label>
                            <frontend_type>text</frontend_type>
                            <sort_order>20</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </username>
                    </fields>
                </general>
            </groups>
        </techandhouse>
    </sections>
</config>

After reloading you will see your two new fields as seen in the screenshot below:

Now, if we want to give users specific options and state what our drop down options are going to be we have to do a little more work. Lets say we wanted the site administrator to set the amount of tweets to cache in the database and set the amount of time to expire the database and session caches we would perform the following steps:

Under the model folder (/app/code/local/TechAndHouse/Tweet/model/) create the following files: Database.php, Session.php and Store.php. Each file will hold options to their pertaining select box in the administration panel:

Database.php

<?php
class TechAndHouse_Tweet_Model_Database
{
    public function toOptionArray()
    {
        return array(
            array('value'=>5, 'label'=>Mage::helper('tweet')->__('5')),
            array('value'=>10, 'label'=>Mage::helper('tweet')->__('10')),
            array('value'=>15, 'label'=>Mage::helper('tweet')->__('15')),            
            array('value'=>20, 'label'=>Mage::helper('tweet')->__('20')),          
            array('value'=>25, 'label'=>Mage::helper('tweet')->__('25')),          
            array('value'=>30, 'label'=>Mage::helper('tweet')->__('30')),          
            array('value'=>35, 'label'=>Mage::helper('tweet')->__('35')),          
            array('value'=>40, 'label'=>Mage::helper('tweet')->__('40')),          
            array('value'=>45, 'label'=>Mage::helper('tweet')->__('45')),          
            array('value'=>50, 'label'=>Mage::helper('tweet')->__('50')),          
            array('value'=>55, 'label'=>Mage::helper('tweet')->__('55')),          
            array('value'=>60, 'label'=>Mage::helper('tweet')->__('60')),                  
        );
    }
}

Session.php

<?php
class TechAndHouse_Tweet_Model_Session
{
    public function toOptionArray()
    {
        return array(
            array('value'=>5, 'label'=>Mage::helper('tweet')->__('5')),
            array('value'=>10, 'label'=>Mage::helper('tweet')->__('10')),
            array('value'=>15, 'label'=>Mage::helper('tweet')->__('15')),            
            array('value'=>20, 'label'=>Mage::helper('tweet')->__('20')),          
            array('value'=>25, 'label'=>Mage::helper('tweet')->__('25')),          
            array('value'=>30, 'label'=>Mage::helper('tweet')->__('30')),          
            array('value'=>35, 'label'=>Mage::helper('tweet')->__('35')),          
            array('value'=>40, 'label'=>Mage::helper('tweet')->__('40')),          
            array('value'=>45, 'label'=>Mage::helper('tweet')->__('45')),          
            array('value'=>50, 'label'=>Mage::helper('tweet')->__('50')),          
            array('value'=>55, 'label'=>Mage::helper('tweet')->__('55')),          
            array('value'=>60, 'label'=>Mage::helper('tweet')->__('60')),                    
        );
    }
}

Store.php

<?php
class TechAndHouse_Tweet_Model_Store
{
    public function toOptionArray()
    {
        return array(
            array('value'=>1, 'label'=>Mage::helper('tweet')->__('1')),
            array('value'=>2, 'label'=>Mage::helper('tweet')->__('2')),
            array('value'=>3, 'label'=>Mage::helper('tweet')->__('3')),            
            array('value'=>4, 'label'=>Mage::helper('tweet')->__('4')),          
            array('value'=>5, 'label'=>Mage::helper('tweet')->__('5')),          
            array('value'=>6, 'label'=>Mage::helper('tweet')->__('6')),          
            array('value'=>7, 'label'=>Mage::helper('tweet')->__('7')),          
            array('value'=>8, 'label'=>Mage::helper('tweet')->__('8')),          
            array('value'=>9, 'label'=>Mage::helper('tweet')->__('9')),          
            array('value'=>10, 'label'=>Mage::helper('tweet')->__('10')),                     
        );
    }
}

In your modules system.xml file, add the following fields:

<?xml version="1.0"?>
<config>
    <tabs>
        ...
    </tabs> 
 
    <sections>
        <techandhouse translate="label">
            ...
            <groups>
                <general translate="label">
                    ...
                    <fields>
                         ...
                        <tweets_to_store translate="label">
                            <label>Number Of Tweets To Store In Database</label>
                            <frontend_type>select</frontend_type>
                            <source_model>tweet/store</source_model> 
                            <sort_order>30</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </tweets_to_store>
                        <session_expiry translate="label">
                            <label>Session Expires in (minutes)</label>
                            <comment>Once tweets are loaded into the session, this module will consult the Session for tweets. After the amount of minutes set here expire, the module will consult the database to see if the database has newer tweets stored. This prevents making database calls every time a page is loaded.</comment>
                            <frontend_type>select</frontend_type>
                            <source_model>tweet/session</source_model> 
                            <sort_order>40</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </session_expiry>
                        <database_expiry translate="label">
                            <label>Database cache expires in (minutes)</label>
                            <comment>Once the session is expired and a tweet is requested, the database is consulted to see if newer tweets are available. If new tweets are not available, the database copy is refreshed by calling the Twitter API and loaded into the session. Set the amount of time database sessions are valid here.</comment>
                            <frontend_type>select</frontend_type>
                            <source_model>tweet/database</source_model> 
                            <sort_order>50</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </database_expiry>
                    </fields>
                </general>
            </groups>
        </techandhouse>
    </sections>
</config>

Update (8/13/2011): Make sure your log settings are enabled in System > Developer > Log Settings for next section to work.

Now, if we reload our page we will notice that we get a blank page. To see the error, take a look at your log files. I have tail -f running on the system.log and exception.log under the /var/log/ directory at all times. If we take a look at the system.log we will notice the following error:

2010-10-01T16:58:33+00:00 ERR (3): Warning: include() [function.include]: Failed opening ‘Mage/Tweet/Model/Store.php’ for inclusion (include_path=’/Applications/MAMP/htdocs/techandhouse/app/code/local:/Applications/MAMP/htdocs/techandhouse/app/code/community:/Applications/MAMP/htdocs/techandhouse/app/code/core:/Applications/MAMP/htdocs/techandhouse/lib:.:/Applications/MAMP/bin/php5/lib/php’) in /Applications/MAMP/htdocs/techandhouse/lib/Varien/Autoload.php on line 93

But why is it looking into the Mage package when Store.php was created in the TechAndHouse package? This is because its falling back to the core to look for this file. We have not declared in our config that we are using these modules.

<?xml version="1.0"?>
<config>
    <modules>
        ...
    </modules>
 
    <adminhtml>
        ...
    </adminhtml>
 
    <global>
      <models>
  	    <tweet>
  		    <class>TechAndHouse_Tweet_Model</class>
  		    <resourceModel>tweet_mysql4</resourceModel>
  	    </tweet>
 
            <tweet_mysql4>
                  <class>TechAndHouse_Tweet_Model_Mysql4</class>
                  <entities>
                        <tweet>
                              <table>th_tweet</table>
                        </tweet>
                  </entities>
            </tweet_mysql4>
      </models>
 
      <resources>
            ...
      </resources>
    </global>
</config>

Returning to the admin panel, we still get presented with the blank page. If we take a look at the system.log file, we notice that the error has changed. We are missing the default helper. A lot of magento code expects a default helper, so lets add this as well:

The error:
2010-10-01T17:01:13+00:00 ERR (3): Warning: include() [function.include]: Failed opening ‘Mage/Tweet/Helper/Data.php’ for inclusion (include_path=’/Applications/MAMP/htdocs/techandhouse/app/code/local:/Applications/MAMP/htdocs/techandhouse/app/code/community:/Applications/MAMP/htdocs/techandhouse/app/code/core:/Applications/MAMP/htdocs/techandhouse/lib:.:/Applications/MAMP/bin/php5/lib/php’) in /Applications/MAMP/htdocs/techandhouse/lib/Varien/Autoload.php on line 93

Under /app/code/local/TechAndHouse/Tweet/Helper add the default helper Data.php:

<?php
class TechAndHouse_Tweet_Helper_Data extends Mage_Core_Helper_Abstract
{
}

In the modules config.xml file add:

<?xml version="1.0"?>
<config>
    <modules>
        ...
    </modules>
 
    <adminhtml>
        ...
    </adminhtml>
 
    <global>
      <helpers>
        <tweet>
          <class>TechAndHouse_Tweet_Helper</class>
        </tweet>
      </helpers>
 
      <models>
  	    ...
      </models>
 
      <resources>
            ...
      </resources>
    </global>
</config>

Now if we reload the admin page, we will see our complete configuration options:

Front Controller

Before we get this to do something remotely useful, lets add a front controller to be able to access and test some module model classes.

In the config.xml file, add the following:

<?xml version="1.0"?>
<config>
    <modules>
        ...
    </modules>
 
    <adminhtml>
        ...
    </adminhtml>
 
    <frontend>
      <routers>
        <TechAndHouse_Tweet_TweetRouter>
          <use>standard</use>
          <args>
            <module>TechAndHouse_Tweet</module>
            <frontName>techandhouse-tweet</frontName>
          </args>
        </TechAndHouse_Tweet_TweetRouter>
      </routers>
    </frontend>
 
    <global>
        ...
    </global>
</config>

And add a new controller, IndexController.php in the /app/code/local/TechAndHouse/Tweet/controllers/ folder:

<?php
 
class TechAndHouse_Tweet_IndexController extends Mage_Core_Controller_Front_Action
{   
    public function indexAction() 
    {   
      echo "Hello World!";
    }
 
}
?>

When we go to http://127.0.0.1:8888/techandhouse-tweet we should now see a blank page with Hello World!. Now, before we continue to write the Twitter Model to get our objective done, lets go over some quick concepts on how to read/write to the database and session and how to retrive system configurations that we set up in the administration panel.

Accessing System Configurations In Admin

Change the indexAction in the IndexController to contain the following:

public function indexAction() 
{   
      $config = Mage::getStoreConfig('techandhouse/general');
 
      print_r($config);
 
      echo "Database Expiry:" . $config["database_expiry"] . "<br/>";
      echo "Session Expiry:" . $config["session_expiry"] . "<br/>";
      echo "Tweets To Store:" . $config["tweets_to_store"] . "<br/>";
}

Results:

Array
(
    [enabled] => 1
    [username] => techandhouse
    [tweets_to_store_in_db] => 5
    [tweets_to_store] => 5
    [session_expiry] => 15
    [database_expiry] => 15
)
Database Expiry:15
Session Expiry:15
Tweets To Store:5

Working with Magento Session

Change the indexAction to have the following:

public function indexAction()
{
      echo Mage::getSingleton('core/session')->getRandomVariable(); //will not return anything
      Mage::getSingleton('core/session')->setRandomVariable("hi");
      echo Mage::getSingleton('core/session')->getRandomVariable(); //will return "hi"
 
      Mage::getSingleton('core/session')->setRandomVariable(array("text"=>"hello world","var"=>"goodbye world")); //can even do arrays
      print_r(Mage::getSingleton('core/session')->getRandomVariable()); //will return array("text"=>"hello world","var"=>"goodbye world")
}

Working with Models And Database

Before we can start working with the table we created earlier in this post we need to create a model that will hold all of our business logic and the collection object in case we work with the built in collection classes.

Create Tweet.php in /app/code/local/TechAndHouse/Tweet/model with:

<?php
class TechAndHouse_Tweet_Model_Tweet extends Mage_Core_Model_Abstract
{
    public function _construct() {
      parent::_construct();
      $this->_init('tweet/tweet');
    }
}

Create Tweet.php in /app/code/local/TechAndHouse/Tweet/model/Mysql4/ with:

<?php
class TechAndHouse_Tweet_Model_Mysql4_Tweet extends Mage_Core_Model_Mysql4_Abstract
{
    public function _construct()
    {
        $this->_init('tweet/tweet', 'tweet_id');
    }
}

Create Collection.php in /app/code/local/TechAndHouse/Tweet/model/Mysql4/Tweet/ with:

<?php
class TechAndHouse_Tweet_Model_Mysql4_Tweet_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract 
{
    public function _construct() {
        parent::_construct();
        $this->_init('tweet/tweet');
    }
}

The model directory structure should now look like this:

Now, go back to the IndexController.php and change the indexAction to contain:

Mage::getModel('tweet/tweet')
        ->setTwitterId("1234")
      	->setText("My new tweet!")
      	->save();

Refresh the page (http://127.0.0.1:8888/techandhouse-tweet) and you will have the following created in your database:

Next, lets try reading from the database. Change the indexAction to now contain:

var_dump(Mage::getSingleton('core/resource')
              ->getConnection('core_read')
              ->select()
              ->from(array("t"=>"th_tweet"),array("text"))
              ->where("text like '%tweet%'")
              ->limit($limit)
              ->query()
              ->fetchAll());

Output: array(1) { [0]=> array(1) { ["text"]=> string(13) “My new tweet!” } }

Need to empty the table? Try:

Mage::getSingleton('core/resource')->getConnection('core_write')->query("TRUNCATE TABLE th_tweet");

Check the database, it should now be empty.

Tweet Model – API Call and database caching

Ok, with all the concepts discussed in the section above, reading the following code should be straight forward. When getTweets is called, the function will first check against our table, th_tweet to see if we have a cached copy that has not expired. If no tweets are returned, the API call is made, result stored in the database and retured to the controller:

<?php
class TechAndHouse_Tweet_Model_Tweet extends Mage_Core_Model_Abstract
{
    public function _construct() {
      parent::_construct();
      $this->_init('tweet/tweet');
    }
 
    public function getTweets($limit=1,$username="techandhouse",$database_expiry=15,$tweets_to_store=5) {    
      $tweets = $this->getLastUnexpiredTweets($limit,$database_expiry);
 
      if(empty($tweets)):
        $tweets = $this->refreshTweets($username,$tweets_to_store,$limit);
      endif;
 
      return $tweets;
    }
 
    private function refreshTweets($username,$tweets_to_store,$limit) {
      Mage::getSingleton('core/resource')->getConnection('core_write')->query("TRUNCATE TABLE th_tweet");
 
      $tweets = json_decode(file_get_contents("http://twitter.com/statuses/user_timeline/{$username}.json?count={$tweets_to_store}"),true);
 
      $i = 0;
 
      foreach ($tweets as $tweet):
        Mage::getModel('tweet/tweet')
          ->setTwitterId($tweet['id'])
          ->setText($tweet['text'])
          ->save();
 
        if($i++ < $limit):
          $return[]['text'] = $tweet['text'];
        endif;
      endforeach;      
 
      return $return;
    }
 
    private function getLastUnexpiredTweets($limit=1,$database_expiry) {
      return Mage::getSingleton('core/resource')
                   ->getConnection('core_read')
                   ->select()
                   ->from(array("t"=>"th_tweet"),array("text"))
                   ->where("updated BETWEEN DATE_SUB(NOW(), INTERVAL {$database_expiry} MINUTE) AND NOW()")
                   ->limit($limit)
                   ->query()
                   ->fetchAll();
    }
}

Now, lets test this out. Change the indexAction to have the following code:

$config = Mage::getStoreConfig('techandhouse/general');
 
$tweet = Mage::getModel('tweet/tweet')->getTweets(1,$config["username"],$config["database_expiry"],$config["tweets_to_store"]);
print_r($tweet);
 
/*
 *Result: Array ( [0] => Array ( [text] => New blog post: Cool Off With Panasonic Inverter Air Conditioners http://bit.ly/9Pj1VY ) )
 */

Setting up a Magento Staging Area

Posted on March 16th, 2011 in Magento | 4 Comments »

Copy Production Store

  1. Login to SSH and go to the Document Root for the staging directory

    cd staging
  2. Copy all data from the live site to the staging directory

    cp -R ../public_html/* ../public_html/.htaccess .
  3. Copy production database to staging database

    mysqldump -u PRODUCTION_DBUSER -p PRODUCTION_DBNAME > data.sql
    mysql -u STAGING_DBUSER -p STAGING_DBNAME < data.sql
  4. Change credentials to store staging database configuration in:

    ~/staging/app/etc/local.xml
  5. Run the following sql query on the staging database and update the values to point to the staging url:

    SELECT * FROM `core_config_data` WHERE `path` LIKE '%base_url%';

Update (7/21/2011): Git

Not sure why it took me this long to finally get the Tech And House site on Git, but finally did it today. I’m currently versioning just the app and skin folders and the themes that are in use, everything else has been placed in .gitignore. Since the staging domain is on the same server as the production domain, my hosting provider flips and disables my backups because I’m not allowed to have that many files on the server. I have to make my updates on dev, test on staging and right after pushing to production, I need to clear out my staging server. If you’re in this boat, here are the steps I follow:

Steps on Staging Server:

  1. Make changes on development box, git commit, git push
  2. Prep staging server by pulling production code to staging folder
    cp -R ../public_html/* ../public_html/.htaccess .
  3. Since we keep deleting all the files on the staging server, reset it to the last committed state of the git repo:
    git reset --hard HEAD
  4. Pull changes that were made on the development machine:
    git pull
  5. Set permissions and clear cache and sessions
    find . -type f -exec chmod 644 {} \;
    find . -type d -exec chmod 755 {} \;
    chmod o+w var var/.htaccess includes includes/config.php app/etc
    chmod -R o+w media
    chmod -R 755 var
    rm -rf var/cache
    rm -rf var/session
    chmod 550 mage
    chmod 550 pear
    #chmod 550 pear Magento <= 1.5 chmod 550 mage for Magento >= 1.5
  6. Update database connection details to point to the staging servers db: /app/etc/local.xml

Maybe “Update 2″ will have Capistrano involved handling the deploy as well, but for now, I’m ok with this little setup. Usually just skip staging all together as thats the most time consuming process due to hosting providers TOS. Change/Test on dev, git pull straight to production.

Excel: Conditional Formatting Row Color Based On Cell Value

Posted on January 25th, 2011 in Random | No Comments »

At times when you’re staring at an Excel spreadsheet for a while it is nice to have certain rows stand out based on certain conditions. A little Excel feature I use every now and then is the conditional formatting found in menu item Format > Conditional Formatting…

If you would just like to highlight a cell, use the “Cell value is” condition and edit the formatting background. If you would like to highlight the entire row, use the “Formula is” condition with the =INDIRECT() function.

For example, if you would like to highlight all rows where cell C of a row has a value less than 0, use =INDIRECT(“C”&ROW())<0

See screenshots:


[Original Source]

Setting up Magento on Rackspace CouldServer

Posted on December 16th, 2010 in Magento, Ubuntu, Web Development | No Comments »

Objective: Prep a test server for a Magento installation on Ubuntu 10.10

In this example I spun up a quick server on Rackspace that has the bare minimums of Ubuntu installed. So everything needs to get installed. Before getting started, take a look at the server requirements to run Magento and run the magento check utility before attempting to install magento to make sure all required components are installed.

Step 1: Update apt-get

sudo apt-get update

Step 2: Install updatedb. Not necessary, just like having it around.

sudo apt-get install locate

Step 3: Install PHP 5

sudo apt-get install php5

Step 4: Install Apache 2.2

sudo apt-get install apache2

Step 5: Enable Apache Mod Rewrite and restart Apache

sudo a2enmod rewrite
sudo /etc/init.d/apache2 restart

Step 6: Install MySQL 5

sudo apt-get install mysql-server

Step 7: Install required PHP extensions and restart Apache

sudo apt-get install php5-mysql php5-mcrypt php5-gd php5-curl php-pear php5-dev libmysqlclient-dev libcurl3-openssl-dev
sudo /etc/init.d/apache2 restart

Step 8: Setup a virtualhost for the new test site Further Reading

sudo cp /etc/apache2/sites-available/default /etc/apache2/sites-available/techandhouse_dev
sudo vi /etc/apache2/sites-available/techandhouse_dev
#Change the DocumentRoot to point to the new location. Example:
/home/user/techecom_dev/
#Change the Directory directive, replace:
<Directory /var/www/> to <Directory /home/user/techecom_dev/>

Sample config:

<!--Change AllowOverride to All for our site for mod rewrite to work-->
<VirtualHost *:80>
        ServerAdmin webmaster@localhost
        ServerName techandhouse_dev
 
        DocumentRoot /home/techecom_dev/public_html/
        <Directory />
                Options FollowSymLinks
                AllowOverride All
        </Directory>
        <Directory /home/techecom_dev/public_html/>
                Options Indexes FollowSymLinks MultiViews
                AllowOverride All
                Order allow,deny
                allow from all
        </Directory>
 
        ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
        <Directory "/usr/lib/cgi-bin">
                AllowOverride None
                Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
                Order allow,deny
                Allow from all
        </Directory>
 
        ErrorLog ${APACHE_LOG_DIR}/error.log
 
        # Possible values include: debug, info, notice, warn, error, crit,
        # alert, emerg.
        LogLevel warn
 
        CustomLog ${APACHE_LOG_DIR}/access.log combined
 
    Alias /doc/ "/usr/share/doc/"
    <Directory "/usr/share/doc/">
        Options Indexes MultiViews FollowSymLinks
        AllowOverride None
        Order deny,allow
        Deny from all
        Allow from 127.0.0.0/255.0.0.0 ::1/128
    </Directory>
 
</VirtualHost>

Deactivate the old site, activate the new one, and restart Apache.

sudo a2dissite default && sudo a2ensite techandhouse_dev
sudo /etc/init.d/apache2 restart

If you run the magento check utility and go to the IP address or domain of your machine, you should see:

Follow instructions here to move the web site to the test server…

After moving the web site, update a couple values so that the site doesn’t route itself back to the real domain because of the database or cached files:

update core_config_data set value="http://DOMAIN/" where path=’web/unsecure/host’;     
update core_config_data set value="http://DOMAIN/" where path=’web/secure/host’;
cd /path/to/your/magento/site
rm -r var/cache/* var/session/*

Ubuntu: Create new user with sudo access

Posted on December 12th, 2010 in Ubuntu | No Comments »

Objective: Create new user on Ubuntu with sudo access

Create New User

$ adduser username
Adding user `username` ...
Adding new group `username` (1000) ...
Adding new user `username` (1000) with group `username` ...
Creating home directory `/home/username` ...
Copying files from `/etc/skel` ...
Enter new UNIX password: 
Retype new UNIX password: 
passwd: password updated successfully
Changing the user information for username
Enter the new value, or press ENTER for the default
	Full Name []: Username
	Room Number []: 
	Work Phone []: 
	Home Phone []: 
	Other []: 
Is the information correct? [Y/n] y

Add to sudoers file

$ visudo

Add (change username to your username):
username ALL=(ALL) ALL

Under:
root ALL=(ALL) ALL

Thats it. This new user can only do superuser commands via sudo. One thing I am still not sure of is how to do is restrict this user from cd ..‘ing back up and snooping around other directories. If you know how, please let me know. For now, I can continue setting up my test server.

Magento: Custom attributes for customer profiles

Posted on November 30th, 2010 in Magento, Web Development | 1 Comment »

If you’re looking to synchronize your customer, sales order and invoices between your back office system and Magento you’re going to need to store the back offices unique ID within Magento. Unlike products, adding custom attributes and attribute sets is not so straight forward. You can not just add it within the administration interface.

Task: Add field erp_customer_id to customer profile (same concept applies to sales orders and invoices)

Step 1: Let Magento know we are creating a new module and add TechAndHouse_Customer.xml in /app/etc/modules/ with the following:

<config>
	<modules>
		<TechAndHouse_Customer>
			<active>true</active>
			<codePool>local</codePool>
		</TechAndHouse_Customer>
	</modules>
</config>

Step 2: Create a new module under /app/code/local/ with the following directory structure:

Step 3a: Copy the contents of /app/code/core/Mage/Customer/etc/config.xml into /app/code/local/TechAndHouse/Customer/etc/config.xml and change:

<modules>
        <Mage_Customer>
            <version>x.x.x</version>
        </Mage_Customer>
</modules>

to

<modules>
        <TechAndHouse_Customer>
            <version>1.0.0</version>
        </TechAndHouse_Customer>
</modules>

Step 3b: In this same file, config.xml add the variable erp_customer_id into the global fieldset under customer_account

<erp_customer_id><create>1</create><update>1</update></erp_customer_id>

Step 4a: Copy the getDefaultEntities method from /app/code/core/Mage/Customer/Model/Entity/Setup.php file to /app/code/local/TechAndHouse/Model/Entity/Setup.php and put it in the following class:

class TechAndHouse_Customer_Model_Entity_Setup extends Mage_Customer_Model_Entity_Setup {

Step 4b: Add the following attribute in the array right before website_id

'erp_customer_id' => array(
                  	'label'		=> 'ERP Customer ID',
                  	'visible'	=> true,
                  	'required'	=> true,
                  ),

Step 5: Add the following in any theme file and visit the page, like /template/customer/form/register.phtml to create the attribute in the database:

$setup = new TechAndHouse_Customer_Model_Entity_Setup('core_setup');
$setup->addAttribute('customer', 'erp_customer_id', array(
	'label'		=> 'ERP Customer ID',
	'type'		=> 'varchar',
	'input'		=> 'text',
	'visible'	=> true,
	'required'	=> true,
	'position'	=> 1,
	));

Step 6: Check if the variable got created in the eav_attribute table.

Step 7: Did it work? At this point you can run a quick test and see if you can write and read from your new attribute. Never modify core files, and do not do anything on a production machine. The following was done on a development box.

Add the following to /design/adminhtml/default/default/template/customer/tab/view.phtml:

$this->getCustomer()->setErpCustomerId("1234");
print_r($this->getCustomer()->getErpCustomerId());

Visit a customer in the admin section and you will see the following:

Credit

Original Article: Fontis

Index Manager Error On Magento

Posted on October 18th, 2010 in Magento | 6 Comments »

The Problem

After performing an upgrade to version 1.4.1.0 my index management and catalog search stopped working.

The Alert:
One or more of the Indexes are not up to date: Catalog Search Index. Click here to go to Index Management and rebuild required indexes.”

The Error:
“Cannot initialize the indexer process”

The Screenshot:

Whats going on?

A table in MySQL has a limitation of 65535 bytes of overall row length. It may severely limit the number of columns in a table when it has varchar/char columns. In Magento starting from 1.3 the products catalog in the “flat” mode suffers from this limitation depending on the number and combination of the product attributes that participate in the flat product index. Source: SUP-MySQLLimitationsontheFlatCatalogProduct-29Jul10-0343PM-17

The workaround that stood out was setting “Used in Product Listing” = No:

Apparently, when I originally set up the store I was under the impression that this field always has to be set to Yes. Of course I want this attribute to be used in my product listing. Whats the point of creating an attribute that will not be used in the product listing, right? Well, no. This field controls if the attribute will be used in the “grid” or “list” view when showing multiple products per page. In my opinion you only need certain fields set to yes for those views, and its already preset when you set up magento for the first time. Fields like, price, special price, name and short description. Not EVERY attribute!

Solution

Take a look at the catalog_eav_attribute and eav_attribute tables. Update the used_in_product_listing field to 0 for all user defined fields that are set to 1. Before running this, use a SELECT clause and see what fields its pulling out. Also, run on a test environment first!!

update `catalog_eav_attribute` as cea left join eav_attribute as ea on cea.attribute_id = ea.attribute_id set cea.used_in_product_listing = 0 where cea.used_in_product_listing = 1 and is_user_defined = 1

…and Fixed!

With a working search…

Another solution

I was trying to find the thread where I found the original PDF to give credit to the person who led me in this direction and ended up stumbling across another solution: http://www.sonassi.com/knowledge-base/magento-knowledge-base/mysql-limitations-on-the-flat-catalogue-in-magento/. If my solution does not help, maybe Sonassis solution will.

Good Luck!

Inode Usage Exceeds Hosting TOS

Posted on October 16th, 2010 in Magento | No Comments »

Periodically I get an email from my hosting provider alerting me that my inode usage exceeds their resource abuse policy. Since I’m using magento on the server they recommend clearing out the contents of downloader/pearlib/cache/* and downloader/pearlib/download/* and deleting all empty .svn folders. My inode usage was still a little higher than necessary after completing these steps.

First of all, what is an inode? An inode stores basic information about a regular file, directory, or other file system object. So in a nutshell, any file, directory or file system object counts toward a point against your inode usage. Messing around I decided to clear all my caches (css, js and images). The image cache really penalized me. By clearing out the image cache my inode usage dropped from 56K to about 40K. Now, this was obviously a temporary fix as my cache is going to be rebuilt as visits come through.

One thing I did realize though was my media folder had high inode usage. Apparently when I was doing my initial product imports using the importing tools I kept on creating image files that were not really used on the site. Why keep those files around? I wrote a little script to clear those images out. Right now my inode usage has dropped to 26K and I don’t have to worry about getting suspended by my hosting provider for a little bit more time.

Before I post the script, a couple things… Test it out in a test environment before running it in production. Make a back up of your site and your image folder in case you miss something and go off deleting a bunch of random files. This is a simple script I put together to remove any unused product images in the site. I threw it up very quickly, I know it could be cleaned up quite a bit with added features and a lot of good blah blah, but I needed to bring my inode usage down quick and this script helped me clear a lot of unused images. Don’t track me down if something bad happens to your site. BACKUP! TEST! You have been warned!

Put the following two files in the /media/catalog/product/ folder:

1) Export contents of the catalog_product_entity_media_gallery table with only the value field:

2) Run the following code in the product folder. Before running the commented out code with the rm statements, see what its doing and test properly! This script will drill down into every subdirectory within the product directory looking for images and then will compare against the export file to see if the image is being used or not. If the image is not being used, the image will be deleted.

<?php
function removeUnusedImages($images,$dir) {
  $prefix = $dir . '/';
  if($handle = opendir($dir)):
    while (false !== ($file = readdir($handle))):
      if ($file === '.' || $file === '..' || $file === 'placeholder' || $file === 'cache') continue;
      $file = $prefix . $file;
      if (is_dir($file)):
        removeUnusedImages($images,$file);
      else: 
        $file = substr($file,1);
        if(!in_array($file,$images)):
          echo "Remove: {$file} \n";
          //system('ls ' . substr($file,1));
          //system('rm ' . substr($file,1));
        else: 
          echo "Dont Remove: {$file} \n";
        endif;
      endif;
    endwhile;
    closedir($handle);
  endif;
}
 
$images = file('export.txt',FILE_IGNORE_NEW_LINES);
removeUnusedImages($images,'.');
?>

Good luck!

Third World Email Solutions

Posted on August 12th, 2010 in Random | No Comments »


I find it difficult to believe that companies in Panama are still using their internet service providers email addresses as their email solution. Email addresses like, company@cwpanama.net or company@gmail.com are perfectly normal to find here. At these companies, one lucky person has access to all inbound company email or, multiple people access the same account. I’m sure you see the problem with this. If you know of a company guilty of this, show them the light and get a Google App account configured. Not only can you create as many users as you want, you can create mailing lists like sales@company.com which is routed to multiple recipients. Best of all your company email will be backed by the power of gmail and all its features! If you need help, I’m willing to assist.

CakePHP: Session value is favicon.ico, Why?

Posted on June 28th, 2010 in Web Development | No Comments »

It was time to give CakePHPs Auth component a little test drive. Thinking I followed the manual to the key, there was no way I missed a step. I mean, there isn’t really much to do to set up simple authentication using CakePHP right? Well, for some reason after logging in I would get pushed to http://localhost:8888/users/favicon.ico:

Anyway, googling, asking for help on #cakephp didn’t really help as the problem was so stupid, not many people have had this problem. I stumbled across this post on the CakePHP google group.

Basically – “make sure the link to favicon.ico is root-relative (/favicon.ico). Otherwise the server’s looking for /controller/action/favicon.ico and the broken link gets captured as the current page. Same thing for your css and js.” – Chris Cassell

(Thank you sir!)