11 October 2011

Magento - working with BST

If you run a Magento store in the UK, you have probably run into the problem of store times being wrong for half of the year as the timezone switches between GMT and BST. Unfortunately Magento has no support for BST at all and the only choice for store owners in the UK is to use standard GMT for the whole year. This of course results in all of the store times being an hour behind over the summer months.

If you have ever looked into the way Magento actually generates times for your store, you probably know that it works from a base of using UTC throughout, and then calculates the time difference between this universal time and the timezone you have selected in admin. The idea behind this is to allow you to set different timezones per store or website and have them all work happily together because in fact they are all actually stored in UTC.

Unfortunately the only way to allow proper support for BST is to change this default timezone from UTC to Europe/London. So a word of warning, if you are in any way planning to use timezones other than Europe/London in your install, then you should not implement the changes here - you are unfortunately probably just going to have to put up with your store being an hour out for half of the year. If that's not you however, read on...

All of the changes here are very simple, start by copying the following core files:
app/code/core/Mage/Core/Model/Locale.php
app/code/core/Mage/Core/Model/Date.php
app/code/core/Mage/Cron/Model/Schedule.php
to their equivalent location under the local branch if they don't already exist there:
app/code/local/Mage/Core/Model/Locale.php
app/code/local/Mage/Core/Model/Date.php
app/code/local/Mage/Cron/Model/Schedule.php
Editing the first two files will change the storing of most timestamps in the database and backend to allow for BST (although we will be making another edit later on), and the third file will correct cron schedule timings (otherwise you will find jobs only queue if they scheduled an hour out).

So, open app/code/local/Mage/Core/Model/Locale.php and find the following line:
const DEFAULT_TIMEZONE  = 'UTC';
Change it to:
const DEFAULT_TIMEZONE  = 'Europe/London';
In the same file, find the following lines in the storeTimeStamp() method:
@date_default_timezone_set($timezone);
$date = date('Y-m-d H:i:s');
@date_default_timezone_set($currentTimezone);
Change them to the following:
@date_default_timezone_set(self::DEFAULT_TIMEZONE);
$date = date('Y-m-d H:i:s');
@date_default_timezone_set(self::DEFAULT_TIMEZONE);
Next open app/code/local/Mage/Core/Model/Date.php and find the following lines in the calculateOffset() method:
$result = date_default_timezone_set($timezone);
and
date_default_timezone_set($oldzone);
Change them to:
$result = date_default_timezone_set(Mage_Core_Model_Locale::DEFAULT_TIMEZONE);
and
date_default_timezone_set(Mage_Core_Model_Locale::DEFAULT_TIMEZONE);
As you can probably see, all we have done is change the class constant for the timezone to Europe/London, and then referenced this in the other edits we have made.

Now you need to make sure cron continues to work correctly, so open app/code/local/Mage/Cron/Model/Schedule.php and find the following line:
$d = getdate(Mage::getSingleton('core/date')->timestamp($time));
This is where cron decides what the current time is, and therefore whether or not to queue your jobs. If we leave this as it is, your jobs won't queue when you schedule them to - instead we just want to pull the server time here without an offset, so change it to the following:
$d = getdate();
Obviously you need to make sure that the server is set to Europe/London also or this won't work.

Thats pretty much all the changes that are needed except that the guys at Varien decided they would put one more timezone reference in a file that you can't override in the local branch. So unfortunately to complete the process we need to make a change in, what I would consider to be a core file. Doing that is definitely not good practice and should be avoided unless absolutely necessary. Unfortunately I don't think there is an alternative in this case, so open app/Mage.php and find the following line:
date_default_timezone_set('UTC');
Change it to:
date_default_timezone_set('Europe/London');
Your store should now be running correctly in the Europe/London timezone and all new content should be recorded with this timestamp. Note however that existing content with a timestamp will not have changed from UTC (being the same as GMT) so will still show as an hour behind during the summer months.

7 October 2011

Magento - disabled modules and block rewrites

Often you can come across page loading issues by disabling a module in admin which uses block rewrites. Here, disabling the module does not actually disable any block rewrites, but it does stop block HTML from being generated - this creates a conflict of interests and stops pages that have module related block content from loading correctly. Also it doesn't create an exception so you can't be sure when it has happened for a customer.

This happens because the module config file is still loaded even when disabled (unlike after setting a particular modules active tag to false in app/etc/modules/Namespace_Module.xml). This in turn means that there could be some quite serious implications on the site frontend and any number of pages could fail to render correctly.

Now it's true that you can just set the modules active tag to false to truly disable the module and circumvent any problems of this type, but if you are not a developer then you may not even know this is an option, or if you do you may not be confident enough to want to risk changing it. Either way I think that an average store owner will probably head straight to admin when wanting to disable a module, and expect that action to actually disable it.

To achieve this we need to stop the module config file from being loaded when it is disabled in admin, and that is what is covered here.

The first step is to copy:
app/code/core/Mage/Core/Model/Config.php
to:
app/code/local/Mage/Core/Model/Config.php
if it's not already there. Open the copied file and look for the following lines:
$modules = $this->getNode('modules')->children();
foreach ($modules as $modName=>$module) {
We are going to need to add code between these two lines, and also directly after (before the if statement that follows).

Add the following code between the 2 lines:
try
{
    $get_db = get_object_vars(Mage::getConfig()->getResourceConnectionConfig("default_setup"));
    $get_core_table = Mage::getSingleton('core/resource')->getTableName('core_config_data');

    $get_connection = mysql_connect($get_db['host'], $get_db['username'], $get_db['password']);
    mysql_select_db($get_db['dbname']);

    $get_results = mysql_query("SELECT path, value FROM `" . $get_core_table . "` WHERE `path` LIKE 'advanced/modules_disable_output/%'");

    $get_status = array();

    while($get_result = mysql_fetch_array($get_results, MYSQL_ASSOC))
    {
        $get_status[$get_result['path']] = $get_result['value'];
    }

    mysql_close($get_connection);
}
catch (Exception $e) { }
So lets step through what is going on here. Firstly, the information about the enabled status of a module as set in admin is not available in any config file (and so is not loaded by Magento) and instead exists only in the core_config_data table of the database you are running Magento from. That means we are going to have to run a database query and pull this data before we decide whether or not to load a particular config file.

So, the first thing we want to do is to put everything inside a try catch block to add some exception handling and allow the code to fail gracefully should the worst happen.

Next we pull the database connection details and put them into an array:
$get_db = get_object_vars(Mage::getConfig()->getResourceConnectionConfig("default_setup"));
After this we request the table name for core_config_data, being the table we are going to be running our query against. It's important to not just use core_config_data directly as the install could be using a table prefix - this will return the table name with that prefix should there be one.

At this point we are not able to query the database in the standard Magento fashion as the necessary classes have not been instantiated, so we are going to have to send a good old fashioned raw SQL statement. We start by opening a connection to the database and selecting the database we want to query:
$get_connection = mysql_connect($get_db['host'], $get_db['username'], $get_db['password']);
mysql_select_db($get_db['dbname']);
Next we store the query that will pull the relevant data from the table. If you look at the core_config_data table, you will see the path column contains some content starting with advanced/modules_disable_output/. The value column associated with each of these entries defines whether the module is disabled or enabled in admin - 0 for enabled, 1 for disabled. This is the data we will pull with the following statement:
$get_results = mysql_query("SELECT path, value FROM `" . $get_core_table . "` WHERE `path` LIKE 'advanced/modules_disable_output/%'");
Next we initialise an array we will use to store the results of the query, we then run the query and store each instance of advanced/modules_disable_output/ as the array key:
$get_status = array();

while($get_result = mysql_fetch_array($get_results, MYSQL_ASSOC))
{
    $get_status[$get_result['path']] = $get_result['value'];
}
After that we close our now redundant database connection, and the try catch block is also closed (we don't do anything in the case of an exception being thrown).

With that added that you should now have the following:
$modules = $this->getNode('modules')->children();

try
{
    $get_db = get_object_vars(Mage::getConfig()->getResourceConnectionConfig("default_setup"));
    $get_core_table = Mage::getSingleton('core/resource')->getTableName('core_config_data');

    $get_connection = mysql_connect($get_db['host'], $get_db['username'], $get_db['password']);
    mysql_select_db($get_db['dbname']);

    $get_results = mysql_query("SELECT path, value FROM `" . $get_core_table . "` WHERE `path` LIKE 'advanced/modules_disable_output/%'");

    $get_status = array();

    while($get_result = mysql_fetch_array($get_results, MYSQL_ASSOC))
    {
        $get_status[$get_result['path']] = $get_result['value'];
    }

    mysql_close($get_connection);
}
catch (Exception $e) { }

foreach ($modules as $modName=>$module) {
Now we need to add some code directly after the opening bracket of the foreach loop. Insert the following code there:
$get_module = 'advanced/modules_disable_output/' . $modName;
$get_status_test = isset($get_status[$get_module]) ? $get_status[$get_module] : 0;
The foreach loop initialises the key $modName, being the current modules name. We can append this to advanced/modules_disable_output/ to create a string that directly correlates to the database results we just pulled and set as each array items key - this is what the first line is doing.

Now we don't want the test we are about to run to fail in the case of a particular module not having a correlating database entry, so the second line by way of a ternary operator pulls the enabled status of the current module, and if there is no entry in the array sets the status to 0 meaning the module should be loaded.

You should now have the following code:
$modules = $this->getNode('modules')->children();

try
{
    $get_db = get_object_vars(Mage::getConfig()->getResourceConnectionConfig("default_setup"));
    $get_core_table = Mage::getSingleton('core/resource')->getTableName('core_config_data');

    $get_connection = mysql_connect($get_db['host'], $get_db['username'], $get_db['password']);
    mysql_select_db($get_db['dbname']);

    $get_results = mysql_query("SELECT path, value FROM `" . $get_core_table . "` WHERE `path` LIKE 'advanced/modules_disable_output/%'");

    $get_status = array();

    while($get_result = mysql_fetch_array($get_results, MYSQL_ASSOC))
    {
        $get_status[$get_result['path']] = $get_result['value'];
    }

    mysql_close($get_connection);
}
catch (Exception $e) { }

foreach ($modules as $modName=>$module) {
    $get_module = 'advanced/modules_disable_output/' . $modName;
    $get_status_test = isset($get_status[$get_module]) ? $get_status[$get_module] : 0;
Now we have all the data we need in order to decide whether or not to load a modules config file according to it's enabled/disabled status. So the final part of this is to modify the if statement immediately following the two line of code you just added, unchanged it should be this:
if ($module->is('active')) {
$module->is('active') pulls the active status from that modules config file, but we want to also take into account the module status as set in admin, so change it to the following:
if ($module->is('active') && !$get_status_test) {
Now you should find that when you disable a module in admin, it is actually, fully, disabled including block rewrites. Remember that if you are not seeing the affects of this within admin, log out and then back in again.