Category Archive:

DDQuery – little class to help with database queries

0

After spending much time coding queries in Zen Cart I can say that database layer object ($db, includes/classes/db/mysql/query_factory.php) is not quite developer friendly when it comes to ease of use. Let’s have a look at typical query to database using this object:

$cid = 12345; // any customer id
$cur = "USD";
$query = "SELECT * FROM " . TABLE_ORDERS . " WHERE customers_id = :cid AND currency= :currency";
$query = $db->bindVars($query, ":cid",$cid,"integer");
$query = $db->bindVars($query,":currency",$cur,"string");
$result = $db->Execute($query);
while(!$result->EOF) {
        echo $result->fields["customers_id"];
        $result->MoveNext();
}

Why it is inconvenient? First of all binding variables to query string takes too many characters to write AND You have to bind one value at a time. Imagine that You have to bind ten variables – it would take ten lines of code just to prepare the query!

Also writing manually each time type of variable (‘integer’,'string’,'date’,'double’) is error prone and inconvenient. I find myself from time to time writing ‘int’ instead of ‘integer’. This is an error that goes unnoticed until the line of code is executed.

The way I want to bind variables is like:

$query = new DDQuery("SELECT * FROM " . TABLE_ORDERS . " WHERE customers_id = :cid AND currency= :currency");
$result = $query->int(":cid",$cid)->string(":currency",$cur)->Query();

It’s just two line of code instead of four and much, much less characters to write. With six variables the code would still take just two lines of code instead of eight. It’s also less error prone to typos since any good IDE will show hint that method ‘sting’ is not defined in DDQuery class.

Iterating through results

I also personally hate the way Zen Cart forces me to iterate through query result. First it’s just hard for me to write !$result->EOF. I would much better prefer to write while($result->next()) (yeah, much like Java java.sql.ResultSet). Second I constantly forgot to write this additional $result->MoveNext() line, especially when while() body has more then five lines of code. So basicly I want to itreate through database results using:

while($result->next()) {
        echo $result->fields["customers_id"])
}

It’s just one line of code less to write but I get rid of ‘!$result->EOF’ which (maybe just to me) is hard to write and in addition I never again will face endless loop.

DDQuery class code

You can download DDQuery class code here. As far as I tested it it works fine. There’s no documentation to the class other then comments in .php file. I don’t have time/I’m too lazy to write it. Still I think that it’s usable piece of code and few of more advanced users of Zen Cart may use it.

Here’s code of the class

<?php

class DDQuery {
    var $query;
   
    public function __construct($query) {
        $this->query = $query;
    }
   
    public function bind($var, $value, $type) {
        global $db;
        $this->query = $db->bindVars($this->query, $var, $value, $type);
        return($this);
    }
   
    public function int($var, $value) { return($this->bind($var, $value, ‘integer’)); }
    public function integer($var, $value) { return($this->bind($var, $value, ‘integer’)); }
   
    public function str($var, $value) { return($this->bind($var, $value, ‘string’)); }
    public function string($var, $value) { return($this->bind($var, $value, ‘string’)); }
   
    public function float($var, $value) { return($this->bind($var, $value, ‘float’)); }
    public function date($var, $value) { return($this->bind($var, $value, ‘date’)); }
    public function currency($var, $value) { return($this->bind($var, $value, ‘currency’)); }
    public function csv($var, $value) { return($this->bind($var, $value, ‘csv’)); }
    public function passthru($var, $value) { return($this->bind($var, $value, ‘passthru’)); }
    public function noquotestring($var, $value) { return($this->bind($var, $value, ‘noquotestring’)); }
    public function enum($var, $value) { return($this->bind($var, $value, ‘enum’)); }
    public function regexp($var, $value) { return($this->bind($var, $value, ‘regexp’)); }
   
    public function getQuery() {
        return($this->query);
    }
   
    public function Execute() {
        global $db;
        return($db->Execute($this->getQuery()));
    }
   
    public function Query() {
        return(new DDQueryResult($this->Execute()));
    }
}

class DDQueryResult {
    var $result;
    var $fields;

    function DDQueryResult($zc_result) {
        $this->result = $zc_result;
    }
   
    function next() {
        if($this->result->EOF) return(FALSE);
        $this->fields = $this->result->fields;
        $this->result->MoveNext();
        return(TRUE);
    }
}
?>

Usage examples

$q = new DDQuery("INSERT INTO " . TABLE_CUSTOMERS_UPLOADS . "(customers_id, original_name, name, last_update, date_added, ip, status, file_size, hash_md5) VALUES(:cid, :o name, :name, NOW(), NOW(), :ip, ‘PENDING’, :size, :md5)");
$q->int(":cid", $cid)->string(":oname",$fileStruct[‘name’])->string(":name",$fileName)->string(":ip",$_SERVER[‘REMOTE_ADDR’])->int(":size",$fileStruct[‘size’])->string(":md5",$md5);
$q->Execute();
$q = new DDQuery("SELECT * FROM " . TABLE_CUSTOMERS_UPLOADS . " WHERE hash_md5 = :md5");
$q = $q->string(":md5", $md5)->Execute();
if($q->EOF) return FALSE;

Installation instructions

  1. Download the class code (DDQuery.zip) and extract it to root folder with Zen Cart installation.
  2. Use it!

Continue Reading

Useful Zen Cart InitSystem breakpoints

0
Here’s list of useful Zen Cart breakpoints taken from includes/auto_load/config.core.php
  • [0] – $zco_notifier is created
  • [10] – $db database object is up and running
  • [40] – configuration values are read from configuration table and defined
  • [60] – functions from includes/functions, includes/functions/extra_functions are loaded
  • [70] – session related functions/classes are loaded, session is created/loaded
  • [80] – shopping cart object is created
  • [90] – currencies are loaded
  • [130] – $messageStack object is created
Posted in: Uncategorized

Continue Reading

Embrace ugliness ;)

0

You know these words…They are like mantra
‘Backup! Backup! Backup!’
One should really do backups before making updates ;) Unfortunately I didn’t and now the site looks like.. well, it’s not pretty ;) It will take me a while to clean up the mess, sorry ;)

Posted in: Uncategorized

Continue Reading

UTI 1.5 is coming.

0

User Tracking Interface version 1.5 will be released next week. There are few important bug fixes, some performance improvements and new features. In nutshell:

  • IP is no longer stored in database as VARCHAR(15) but as INT, so each UTI record takes less space
  • uti table prune is no longer triggered on catalog activity. Prune is triggered by admin activity and is executed only once per day.
  • some indexes on uti table might be changed
  • support for custom uti attributes merge functions (I’ll elaborate on it on UTI 1.5 release)

Continue Reading

Fun with UTI – personalized ‘Welcome back’ message.

0

If You visit any Zen Cart store You may notice greeting message that says something like ‘Welcome Guest! Would you like to log yourself in?‘. Have You ever wondered what’s purpose of this message? Does it give any information to the visitor? Does it make him feel special? Does it serve any purpose other then providing link to ‘Log In’ page? No, it does not. Zen Cart displays personalized greetings like ‘Welcome back, John!‘, but only to registered users who are already logged in.

Default Zen Cart greeting message to Guest visitors.

Compare it to Amazon store. Let’s say that the visitor (John)  has account there and occasionally visits their website. Every time he goes back he is greeted with personalized message: ‘Hello, John. We have recommendations for You.‘. John does not have to log in, provide any information or do anything special – yet he is still recognized as returning customer. How’s that different from Zen Cart greeting?

  • John knows that he already has account at Amazon so he does not have to create one
  • John gets personalized message
  • he has access to his shopping cart without logging in
  • he sees his recently viewed products
  • Amazon recommends some items based on his shopping/browsing history
  • even if the visitor is not John Johns identity and private data is not revealed.

Pretty cool, right? When John visits Amazon.com he feels like he was already logged in (but he’s not). Can Zen Cart to that? Yes, with User Tracking Interface.

UTI to the rescue!

If You’re not familiar with User Tracking Interface (UTI) please read about it here. You will need to install UTI if You want to implement this functionality on Your store.

With UTI We can make Zen Cart a bit more friendly to returning visitors. UTI recognizes returning visitors by cookie, ip address or both and stores basic information about them (time of last visit, last IP address, customer ID if visitor created account).

What We need to do

  • check if UTI recognized the visitor as returning customer (UTI does it automatically)
  • if it did We display personalized message

Code

We’ll be working with default template that comes with Zen Cart: template_default. In case of custom templates the same steps should be applied.

Zen Cart uses zen_customer_greeting() function to display greeting message to the visitor. Here’s snippet of code that uses this function (includes/templates/template_default/templates/tpl_index_default.php), lines 19-21:

<?php if (SHOW_CUSTOMER_GREETING == 1) { ?>
<h2 class="greeting"><?php echo zen_customer_greeting(); ?></h2>
<?php } ?>

Unfortunately We can’t use zen_customer_greeting() as We don’t want to change Zen Cart core code. We’ll create our own greeting function instead. We’ll put it in includes/functions/extra_functions/personalized_greeting.php.

<?php
function personalized_greeting() {
    global $uti;
    global $db;
    // revert back to default greeting function if the visitor is not recognized by UTI
    if(!isset($uti)) return(zen_customer_greeting());
    if( !isset($uti->customerID) || ((int)$uti->customerID) == 0) return(zen_customer_greeting());
   
    // UTI recognizes the visitor as returning customer, let’s get his first name
    $q = $db->Execute("SELECT * FROM " . TABLE_CUSTOMERS . " WHERE customers_id = " . (int)$uti->customerID);
    return(sprintf(TEXT_GREETING_PERSONAL, zen_output_string_protected($q->fields[‘customers_firstname’]), zen_href_link(FILENAME_PRODUCTS_NEW)));

}
?>

We reuse default Zen Cart greeting message, TEXT_GREETING_PERSONAL. We now need to modify our template to use personalized_greeting() instead of zen_customer_greeting(). Go to includes/templates/template_default/templates/tpl_index_default.php and change:

<?php if (SHOW_CUSTOMER_GREETING == 1) { ?>
<h2 class="greeting"><?php echo zen_customer_greeting(); ?></h2>
<?php } ?>

to:

<?php if (SHOW_CUSTOMER_GREETING == 1) { ?>
<h2 class="greeting"><?php echo personalized_greeting(); ?></h2>
<?php } ?>

And that’s it! From now on returning customers will be greeted with personalized message.

Test

Visit Your store and create test account. Close Your browser and return to Your website after half hour. Default  Zen Cart session lifespan is 15 minutes so any saved Zen Cart session should be invalidated by now. If our little module works correctly You should be greeted by Your name.

Personalized greeting message to returning customers (using UTI)

Download

For Your convenience I prepared .zip file with personalized_greeting() function. You can download it here. However, You’ll have to make necessary changes to the template yourself.

Download the package.

Remember that You have to have UTI installed for this module to work.

Posted in: Uncategorized

Continue Reading

A/B Split Test: Is McAffee or WebSafeShield seal worth its price?

0

Many sites have fancy seals like ‘HackerSafe’, ‘WebsSafeShield’,'BuyerShield’ and so on. Their issuers claim that those seals give confidance to customers and they are more likely to buy on site that has security seal. Some seal issuers claim boost of sales around 20%. The question is – is it true that store owner’s get more orders? How to determine that? If You’re store owner and Your store is using Zen Cart You can perform Your own A/B split test to find out if those seals improve sales.

First, do the math

Business is about income – if investment does not earn You more money than it costs You shouldn’t put money into it. Before You purchase security scanning service You should calculate if it is worth to pay $40-$80/month just to get the seal?

Let’s say that Your store has income of $200/month (hey, maybe You’re just starting?). In best case scenario You’ll get 20% boost in sales, which is $40/month. In not-so-optimistic scenario sales will improve by 10% – $20/month. You’re spending $40-$80/month for the seal, and it provides $20-$40/month, so You’re spending more than You earn. In that case You shouldn’t get the seal – it’s just not worth it.

Now, if You get $1000 or more out of Your store things start to look bit different. The seals earns $200/$100 per month in optimistic/not-so-optimistic scenario, so You earn more than You spend. You definitely should get the seal. Keep in mind however that You’re still unsure that the seal will provide more money than it costs. You need to perform A/B split test to know that.

The Test

I’ll show You exactly how to perform such A/B split test using Randomized Tests. You’ll need Randomized Tests contribution which allows You to perform A/B split tests on Your site. Randomized Tests requires also User Tracking Interface contribution which You can find here.

I will be using default Zen Cart template to insert the seal, but steps should be very similar with Your custom template. I also decided to use WebSafeShield seal in the test.

Step 1 – create the groupset

I want perform A/B split test with two test groups:

  • Control Group – visitors in this group will see old version of the page without the seal.
  • Test Group – visitors in this group will see changed header with the seal displayed.

Before I create the test I need first create set of visitor groups – groupset. To do this I go to Admin -> Tools -> [RT] Groupsets and I create ‘Standard Groupset’.

I then click twice on it and create two groups:

  • Control Group (name: ‘Control Group’, id: ‘cg’, control group: checked)
  • Test Group (name: ‘Test Group’, id: ‘tg’, control group: unchecked)

Step 2 – prepare the template

I want the seal to be clearly visible to visitors so I’m going to insert it into header area. With this in mind I change the template file (includes/templates/template_default/common/tpl_header.php) around line 77 from:

<div id="tagline"><?php echo HEADER_SALES_TEXT;?></div>

to:

<div id="tagline"><?php echo HEADER_SALES_TEXT;?></div>
<?php
    if(isset($exp) && $exp->groupID == ‘tg’) {
?>
<div id="securitySeal"><!– Begin WebSafe Shield code –>
<script src="https://www.websafeshield.com/seals/my_domain110x56_com/websafeshield.js"></script>
<!– End WebSafe Shield code –>
</div>
<?php } ?>

Step 3 – create the test

Now You just need to create the test. Go To Admin->Tools->[RT] Experiments and use form on the bottom to create new experiment:

  • Name – enter ‘Seal Test’
  • ID – enter ‘seal_test’
  • Last From – enter current server date, e.g.: 2010-05-21 19:00:00
  • Last To – enter test end date – I recommend entering date far in the future as You can always end the test earlier. Enter 2011-01-01 00:00:00
  • GroupSet – choose ‘Standard’
  • Impression Detection Function – leave as it is (‘default_rt_idf’)
  • Click ‘Create’

Creation of the experiment

View of test stats just after the test has been created

Step 4 – verify that the test is running

Go to Your store and check that the test is running. Make sure that ‘Impressions’ counter in Experiment Statistics has been increased by 1 on Your visit (this counter is increased ONLY ONCE per visitor). Visit Your store from few computers or smartphones – You should be able to see modified version of the header on some (unfortunately Randomized Tests does not provide yet tool to store owners to switch test group – You have to test from several computers or clean cookies to be moved to different test group).

Header of Control Group

Header of Test Group

Header of Test Group

Step 5 – wait for the results

Wait until the test will provide statistically significant results. Result is statistically significant if some group is clear winner – it has 95% or more chance to beat control group and has been fed with enough data. If test group has 5% or less chance to beat control group You may consider control group as clear winner. As the rule of thumb let the test run until:

  • One of groups get 95% chance to be clear winner
  • and there has been at least 100 impressions and 50 order conversions in each group.

Step 6 – do the math again

You finished Your test and have the clear winner. If the clear winner is control group then it means that the seal does more harm than good and Your customers are less willing to buy when the seal is displayed (it would be strange though – maybe HTML code is not properly formatted and it breaks design of Your store?)

If test group (the one to which seal was displayed) is the winner You should check how much more money it earned. If ‘Total Orders Value’ rose by %10 and You earn $1000/month then the seal earns: 0.1 * $1000 = $100/month, so it’s a good idea to keep it.

If You decide to perform the test please post Your results in comments – I’m curious how much such seals boost sales in real life.

Posted in: Uncategorized

Continue Reading

How to create custom impression detection function for Randomized Tests

2

Randomized Tests version 1.3 introduces new feature – custom impression functions. It allows You to decide who and under what conditions participates in A/B split test. In this post I would like to show You how to create custom impression detection function and how to use it to speed up experiments.

The need for custom impression detection functions

Let’s assume that You want to change Your registration page. You hope that new version will ease process of registration and more customers will register/order. You want to include in experiment only those visitors that actually viewed ‘Register’ page and remove from experiment those visitors that never intended to register / make purchase and never visited ‘Register’ page. Why? Because You can get more accurate results more quickly. Let me show it to You on example.

Example

Let’s assume that Your site gets 1000 visits daily (excluding bots). Of those 1000 visitors only 100 intend to buy anything ( other visitors are just surfing, comparing prices or doing anything else – they are not customers). Those 100 visitors get to ‘Register’ page where You  already set up A/B split test with two versions of the page.  You know – from Google Analytics or anywhere else – that old version of the page (version A or control group) has  20% conversion rate. You expect new version of the page to have 40% conversion rate. How quickly will You know that the new page is better? Here’s comparison of default IDF vs custom IDF:

  • Default impression detection function (all visitors are included in the experiment)

Group Visitors Impressions Conversions Conv. Rate Duration
Group #A 500 500 10 2% 6.41 days
Group #B 500 500 20 4%
  • Custom impression detection function (impression occurs only when visitor views ‘Register’ page for the first time)

Group Visitors Impressions Conversions Conv. Rate Duration
Group #A 500 50 10 20% 5.66 days
Group #B 500 50 20 40%

As You can see from this simple example there’s 15% decrease in duration of experiment. In other scenarios improvement may be even better.

How to create IDF (Impression Detection Function)

Randomized Tests can make use of any function that:

  • is located in ‘includes/functions/extra_functions’ directory
  • ends with ‘_rt_idf’ string (e.g. my_custom_rt_idf())
  • takes five arguments in following order:
    • $experimentRowID – ID of row in rt_experiments table representing current experiment
    • $experimentID – ID of current experiment You created (e.g. ‘login_test’)
    • $groupRowID – ID of row in rt_experiments_groups table representing group to which customer has been assigned
    • $groupID – ID above group (e.g. ‘control_group’, ‘test_group’)
    • $newlyAssigned – TRUE if visitor has just been assigned to group (e.g. it’s his first visit to Your store)
  • returns:
    • TRUE – if impression occured
    • FALSE – otherwise

Ok, so let’s create such function and upload it to /includes/functions/extra_functions:

function visited_register_page_rt_idf($e_row_id, $eid, $g_row_id, $gid, $new) {
        global $uti;
        global $current_page_base;</p>
        if($current_page_base == ‘create_account’) {
                $flag = $uti->get(‘visited_create_account_page’);
                if($flag === FALSE || $flag == ‘FALSE’) {
                        $uti->set(‘visited_create_account_page’, ‘TRUE’);
                        return(TRUE);
                }
        }
        return(FALSE);
}

First, function checks if currently visited page is ‘create_account’ page – We want impression to occur only on registration form page (which in Zen Cart is ‘create_account’ page). Impression should occur only once per visitor so function checks using UTI if visitor already viewed ‘create_account’ page. If he didn’t function does two things:

  • saves information that visitor viewed ‘create_account’ page in UTI
  • return TRUE – impression occured

If visitor already visited ‘create_account’ page impression does not occur and function returns FALSE.

Now upload the function to includes/functions/extra_functions and create experiment that uses it to detect impression.

Modify ‘Create Account’ page

Let’s modify ‘Create Account’ page. We just want to test two versions of ‘Create Account’ page so I used ‘Standard Groupset’ (Admin->Tools->[RT] Groupsets) as groupset. Standard Groupset has two groups: control group (id ‘cg’) and test group (id ‘tg’).

if(!isset($exp) || $exp->groupID == ‘cg’) {
        // control group
        // display old ‘Create Account’ page
} else {
        // test group
        // display new version of ‘Create Account’ page
}

If visitor has been assigned to control group (‘cg’) old version of page is displayed. In other case new ‘Create Account’ page is used.

How to use IDF in experiment

Go to Admin->Tools-> [RT] Experiments page. Create new experiment as usually. In ‘Impression Detection Function‘ box paste name of Your impression detection function: ‘visited_create_account_page_rt_idf’  (without braces). Click on ‘Create’. That’s it!

Test that Your IDF works

Visit Your site and check that impression only occurs on first visit to ‘Create Account’ page.

That’s it. If anything isn’t clear please post comment.


Continue Reading

User Tracking Interface v1.1

8

It’s mainly bug fix release. Update is STRONGLY recommended.

Changelog:

  • Changed file name of includes/auto_loaders/config.utis.php.php to includes/auto_loaders/config.utis.php
  • uti_install.sql missed some inserts
  • Fix to: [DELETE FROM uti_attributes WHERE uti_row_id = ? AND name IN () ]
  • UTI tables now use DB_PREFIX

You can download it from: UTI v1.1


Continue Reading

[Solved] FireFox doesn’t save cookies – sort of…

0

Funny thing (and very annoying) – my FireFox 3.5.5 doesn’t save cookies anymore. It saves cookies until browser is running, but after I close it and open it again only those cookies that are meant to be save until ‘end of session’ are preserved. My cookie policy is ok ( cookies are stored until they expire ), cache isn’t cleared after I close FireFox and there are no sites on ‘Exceptions’ list. Clearing cache & purging cookies does not help at all.

Update: I finally found solution – just delete cookies.sqlite and cookies.sqlite-journal from %USERPROFILE%\AppData\Roaming\Mozilla\FireFox\Profiles directory.

Posted in: Uncategorized

Continue Reading

Query Cache v1.7 will be released soon

0

After long break new version of Query Cache for Zen Cart will be released in next week/two weeks. Few of You reported some ‘Out of memory’ errors that could be avoided so new version of QC will focus on memory consumption.  I’m not sure if performance per single http request is going to improve by using less memory – probably it will but possibly not significantly.

Posted in: Uncategorized

Continue Reading

Social Stuff

Donate

If You like Query Cache, Query Log or Randomized Tests please make a $5 donation.

Search the blog