In this tutorial, we will create a Magento module that will capture an affiliate referral from a third-party source (e.g. an external website or newsletter) and include a HTML script on the checkout success page once this referral has been converted.
We will be covering the following topics:
- Capturing URI request data,
- Saving to and retrieving values from cookies,
- Adding custom content to the checkout success page,
- Adding module configuration to the Magento admin panel,
- Creating a community module with no hard-coded settings.
As always, this module will be written in such a way that no core files are modified, making it portable and Magento-upgrade friendly.
Further Reading on SmashingMag:
- Creating Custom Shipping Methods In Magento
- Introducing The Magento Layout
- 15 Common Mistakes in E-Commerce Design
- Fundamental Guidelines Of E-Commerce Checkout Design
Before We Start
This tutorial assumes you are already familiar with the basics of creating a Magento module, and builds upon the points covered in our previous article on the Magento Layout, so it might be worth brushing up on both if you are not familiar with either.
We will be adding any website-specific settings as configurable options in the Magento admin panel, and as a result, our module is a “community module” whose code belongs in app/code/community
, the idea being that we can drop this community module onto any Magento instance, and no code modification will be required to get it working.
To begin with, create the following file structure with empty files ready to be populated later.
app
- code
- community
- SmashingMagazine
- Affiliate
- Block
- Conversion.php
- Model
- Observer.php
- etc
- config.xml
- design
- frontend
- base
- default
- layout
- smashingmagazine_affiliate.xml
- template
- smashingmagazine_affiliate
- conversion.phtml
- etc
- modules
- SmashingMagazine_Affiliate.xml
We can already complete the content of SmashingMagazine_Affiliate.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<config>
<modules>
<SmashingMagazine_Affiliate>
<active>true</active>
<codePool>community</codePool>
</SmashingMagazine_Affiliate>
</modules>
</config>
Capturing The Affiliate Referral
The first task of an affiliate-tracking process is to capture the referral, which is generally provided as a $_GET
parameter in a URI from a third-party website or newsletter. For example smashingmagazine.com/?utm_source=some_affiliate_id
where some_affiliate
is the ID of the affiliate we will reward if our customer completes a purchase.
Defining An Event Observer
We want our affiliates to be able to link to any page of our website, so that our customers can click directly to the product or service of interest. Therefore, we need a method of checking for this $_GET
parameter on every page. As always, we don’t want to modify any Magento core code since we want our module to be as portable and upgrade-friendly as possible, so we will be using an event observer
.
One event that is dispatched on every page is controller_front_init_before
, so let’s create our config.xml
with an observer
for this event.
<?xml version="1.0" encoding="UTF-8"?>
<config>
<modules>
<SmashingMagazine_Affiliate>
<version>0.0.1</version>
</SmashingMagazine_Affiliate>
</modules>
<global>
<models>
<smashingmagazine_affiliate>
<class>SmashingMagazine_Affiliate_Model</class>
</smashingmagazine_affiliate>
</models>
<events>
<controller_front_init_before>
<observers>
<smashingmagazine_affiliate>
<class>smashingmagazine_affiliate/observer</class>
<method>captureReferral</method>
<type>singleton</type>
</smashingmagazine_affiliate >
</observers>
</controller_front_init_before>
</events>
</global>
</config>
Now, on every page load, our method SmashingMagazine_Affiliate_Model_Observer::captureReferral()
will be called, so let’s add our Observer.php
content:
<?php
class SmashingMagazine_Affiliate_Model_Observer
{
public function captureReferral(Varien_Event_Observer $observer)
{
// here we add the logic to capture the referring affiliate ID
}
}
Capturing the Affiliate ID
The event controller_front_init_before
is dispatched from the Magento core method Mage_Core_Controller_Varien_Front::init()
, and includes a reference to the same class in the dispatched event, as per the following code snippet from app/code/core/Mage/Core/Controller/Varien/Front.php
:
Mage::dispatchEvent(
'controller_front_init_before',
array('front' => $this)
);
We can therefore gain access to this instance of Mage_Core_Controller_Varien_Front
by including the following code in our event observer
:
<?php
class SmashingMagazine_Affiliate_Model_Observer
{
public function captureReferral(Varien_Event_Observer $observer)
{
$frontController = $observer->getEvent()->getFront();
}
}
Since the controller
is responsible for the parsing of URI’s, we now have access to the appropriate object we need to determine whether our $_GET
parameter exists:
<?php
class SmashingMagazine_Affiliate_Model_Observer
{
public function captureReferral(Varien_Event_Observer $observer)
{
$frontController = $observer->getEvent()->getFront();
$utmSource = $frontController->getRequest()
->getParam('utm_source', false);
if ($utmSource) {
// here we will save the referrer affiliate ID
}
}
}
In the above code, we are calling getRequest()
to retrieve the Mage_Core_Controller_Request_Http
instance from the controller
, which will contain all of the information we need about the current URI request, including any $_POST
or $_GET
parameters. We are interested in the $_GET
parameter utm_source
, and we can retrieve its value with the getParam()
method.
If present in the current URI, the variable $utmSource
will now contain our referring affiliate’s ID.
Saving the Affiliate ID
Now we need to save the $utmSource
value so that we can reward this affiliate should our customer proceed to make a purchase. There are a number of ways to do this, but for this tutorial we will simply use a $_COOKIE
. Magento has core functionality for dealing with cookie
s, so this is very straightforward:
<?php
class SmashingMagazine_Affiliate_Model_Observer
{
const COOKIE_KEY_SOURCE = 'smashingmagazine_affiliate_source';
public function captureReferral(Varien_Event_Observer $observer)
{
$frontController = $observer->getEvent()->getFront();
$utmSource = $frontController->getRequest()
->getParam('utm_source', false);
if ($utmSource) {
Mage::getModel('core/cookie')->set(
self::COOKIE_KEY_SOURCE,
$utmSource,
$this->_getCookieLifetime()
);
}
}
protected function _getCookieLifetime()
{
$days = 30;
// convert to seconds
return (int)86400 * $days;
}
}
We have defined a new constant, COOKIE_KEY_SOURCE
, because we will be referencing this same value from a different class. When referencing a value like this from multiple locations, it is good practice to use a constant. This way, we keep refactoring work to a minimum should the value ever need to change. We have also defined a new protected method, _getCookieLifetime()
, for retrieving the cookie
lifetime, which we have hard coded to 30 days from now.
Notifying The Affiliate Upon Conversion
At this point, we now have an affiliate ID stored in a cookie
, and let’s assume our referred customer has added some products to their basket, completed the checkout process and is now on the checkout success page. This is where most affiliate schemes will require some notification that a referral has been converted to an order, usually by way of including a snippet of JavaScript or a tiny image at the bottom of the page.
There are, of course, other methods of updating affiliates, for example via server-side API calls which can be achieved with further event observers
, but for this tutorial we will utilize the Magento layout to introduce some custom HTML at the bottom of the order success page.
We need to update our config.xml
to cater to blocks
and layout updates:
<?xml version="1.0" encoding="UTF-8"?>
<config>
<modules>
<SmashingMagazine_Affiliate>
<version>0.0.1</version>
</SmashingMagazine_Affiliate>
</modules>
<global>
<blocks>
<smashingmagazine_affiliate>
<class>SmashingMagazine_Affiliate_Block</class>
</smashingmagazine_affiliate>
</blocks>
<models>
<smashingmagazine_affiliate>
<class>SmashingMagazine_Affiliate_Model</class>
</smashingmagazine_affiliate>
</models>
<events>
<controller_front_init_before>
<observers>
<smashingmagazine_affiliate>
<class>smashingmagazine_affiliate/observer</class>
<method>captureReferral</method>
<type>singleton</type>
</smashingmagazine_affiliate >
</observers>
</controller_front_init_before>
</events>
</global>
<frontend>
<layout>
<updates>
<smashingmagazine_affiliate
module="SmashingMagazine_Affiliate">
<file>smashingmagazine_affiliate.xml</file>
</smashingmagazine_affiliate>
</updates>
</layout>
</frontend>
</config>
Adding a New Layout File
The layout handle we are interested in is onepage_checkout_success
, since this is the handle for the checkout success page that our customer has arrived at. Let’s update our layout file, smashingmagazine_affiliate.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<layout>
<checkout_onepage_success>
<reference name="before_body_end">
<block type="smashingmagazine_affiliate/conversion"
name="smashingmagazine_affiliate_conversion"
template="smashingmagazine_affiliate/conversion.phtml" />
</reference>
</checkout_onepage_success>
</layout>
Adding A New Template File
The content of conversion.phtml
will depend on the requirements of the specific affiliate we are building the module for, so for now we will use a generic format that includes a one pixel image with a dynamic merchant and affiliate ID:
<img
src="/themes/smashingv4/images/logo.png
?merchant_id=<?php echo $this->getMerchantId() ?>
&affiliate_id=<?php echo $this->getAffiliateId() ?>"
width="1"
height="1"
/>
Creating a Custom Block
Now we need to create a new block
for populating the dynamic data in our template:
<?php
class SmashingMagazine_Affiliate_Block_Conversion
extends Mage_Core_Block_Template
{
public function getMerchantId()
{
return '12345';
}
public function getAffiliateId()
{
return Mage::getModel('core/cookie')->get(
SmashingMagazine_Affiliate_Model_Observer::COOKIE_KEY_SOURCE
);
}
}
Adding Items To The Magento Admin Panel
At this point, we have a working module. All of our functionality is in place, and our hard-coded values will work for a single instance of Magento. We have the equivalent of a local module. The next step is to move these hard-coded values into the Magento admin panel, so they can be configured on multiple Magento instances without any code modification required.
It is quite straightforward to add items to the Magento admin panel system configuration, and it’s ideal for saving website-specific credentials or settings, like in our case the merchant ID and cookie
timeout value.
Now, let’s log in to the Magento admin panel and navigate to System
→ Configuration
using the main menu. We can see a number of tabs on the left-hand side for configuring the various elements of our Magento instance, such as “General,” “Web,” “Design,” etc. We are now going to add a new tab for our module’s configuration items.
Adding New System Configuration Tab
Let’s create a new XML file called app/code/community/SmashingMagazine/Affiliate/etc/system.xml
and add to it the following content:
<?xml version="1.0" encoding="UTF-8"?>
<config>
<!-- we are defining a new tab -->
<tabs>
<!-- our tab unique short name -->
<smashingmagazine>
<!-- the title of our tab in the admin panel sidebar -->
<label>Smashing Magazine</label>
<!-- the order our tab should appear on the sidebar -->
<sort_order>100</sort_order>
</smashingmagazine>
</tabs>
</config>
Configuring ACL Settings
Since we have added a new tab, we need to add a new entry to the Magento ACL (Access Control List) to allow access to this tab. Let’s create a new file called app/code/community/SmashingMagazine/Affiliate/etc/adminhtml.xml
, with the following content:
<?xml version="1.0" encoding="UTF-8"?>
<adminhtml>
<acl>
<resources>
<admin>
<children>
<system>
<children>
<config>
<children>
<smashingmagazine_affiliate>
<title>Smashing Magazine
Affiliate</title>
</smashingmagazine_affiliate>
</children>
</config>
</children>
</system>
</children>
</admin>
</resources>
</acl>
</adminhtml>
Note: When making changes to the ACL, if you are already logged in to Magento you will have to log out and back in again for the new permissions to take effect. You may find you get a “404 page not found” when you try to access a newly-added tab that you do not have ACL access to.
Adding New System Configuration Items
Next we will update our system.xml
to add the items that we want to be configurable:
<?xml version="1.0" encoding="UTF-8"?>
<config>
<tabs>
<smashingmagazine>
<label>Smashing Magazine</label>
<sort_order>100</sort_order>
</smashingmagazine>
</tabs>
<!-- we are adding a new section to our tab -->
<sections>
<!-- unique shortname for our section -->
<smashingmagazine_affiliate>
<!-- the title of our section in the sidebar -->
<label>Affiliate Tracking</label>
<!-- the tab under which we want our section to appear -->
<tab>smashingmagazine</tab>
<!-- order of section relative to our tab -->
<sort_order>10</sort_order>
<!-- visibility of our section -->
<show_in_default>1</show_in_default>
<show_in_website>1</show_in_website>
<show_in_store>1</show_in_store>
<!-- we are adding some new groups to our section -->
<groups>
<!-- the unique short code for our group -->
<general>
<!-- the title of our group -->
<label>General Settings</label>
<!-- order of group relative to the section -->
<sort_order>10</sort_order>
<!-- visibility of our group -->
<show_in_default>1</show_in_default>
<show_in_website>1</show_in_website>
<show_in_store>1</show_in_store>
<!-- we are adding some new fields to our group -->
<fields>
<!-- the unique short code for our field -->
<status>
<!-- the label of our field -->
<label>Enabled</label>
<!-- the type of our field -->
<frontend_type>select</frontend_type>
<!-- the source of our 'select' type -->
<source_model>
adminhtml/system_config_source_enabledisable
</source_model>
<!-- order relative to the group -->
<sort_order>10</sort_order>
<!-- visibility of our field -->
<show_in_default>1</show_in_default>
<show_in_website>1</show_in_website>
<show_in_store>0</show_in_store>
</status>
<merchant_id>
<label>Merchant ID</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>
</merchant_id>
</fields>
</general>
<cookie>
<label>Cookie Settings</label>
<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>
<fields>
<timeout>
<label>Cookie Timeout</label>
<frontend_type>text</frontend_type>
<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>
<!--
we can add a comment that
will appear below the field
-->
<comment><![CDATA[
This is the amount of time
an affiliate cookie will last, in days
]]></comment>
</timeout>
</fields>
</cookie>
</groups>
</smashingmagazine_affiliate>
</sections>
</config>
Defining Default Admin Panel Settings
Sometimes it is convenient to provide default values for your admin panel configuration settings. For example, it would be a good idea to provide a default value for the cookie
timeout value. However, there may be no benefit in providing a default value for merchant_id
, since it will always be different. Let’s update our config.xml
with a default cookie
timeout value:
<?xml version="1.0" encoding="UTF-8"?>
<config>
<modules>
<SmashingMagazine_Affiliate>
<version>0.0.1</version>
</SmashingMagazine_Affiliate>
</modules>
<global>
<blocks>
<smashingmagazine_affiliate>
<class>SmashingMagazine_Affiliate_Block</class>
</smashingmagazine_affiliate>
</blocks>
<models>
<smashingmagazine_affiliate>
<class>SmashingMagazine_Affiliate_Model</class>
</smashingmagazine_affiliate>
</models>
<events>
<controller_front_init_before>
<observers>
<smashingmagazine_affiliate>
<class>smashingmagazine_affiliate/observer</class>
<method>captureReferral</method>
<type>singleton</type>
</smashingmagazine_affiliate >
</observers>
</controller_front_init_before>
</events>
</global>
<frontend>
<layout>
<updates>
<smashingmagazine_affiliate
module="SmashingMagazine_Affiliate">
<file>smashingmagazine_affiliate.xml</file>
</smashingmagazine_affiliate>
</updates>
</layout>
</frontend>
<!-- we are setting the default value -->
<default>
<!-- this is the section short name -->
<smashingmagazine_affiliate>
<!-- this is the group short name -->
<cookie>
<!-- this is the field short name -->
<timeout>30</timeout>
</cookie>
</smashingmagazine_affiliate>
</default>
</config>
Accessing The System Configuration Values
Once all of the above is configured, and we have our default values in place, or have saved our settings through the admin panel, we can easily access these values with the following snippet:
<?php $value = Mage::getStoreConfig('system/config/path'); ?>
The system/config/path
should be replaced with the configuration path to the particular setting you are interested in. For example, the path to our merchant_id
setting would be smashingmagazine_affiliate/general/merchant_id
.
Retrieving Our Cookie Timeout From Admin Panel
We previously had the cookie
lifetime hard-coded to 30 days, which we can now replace with our Magento admin panel system configuration value:
<?php
class SmashingMagazine_Affiliate_Model_Observer
{
const COOKIE_KEY_SOURCE = 'smashingmagazine_affiliate_source';
public function captureReferral(Varien_Event_Observer $observer)
{
$frontController = $observer->getEvent()->getFront();
<!-- unique shortname for our section -->
<smashingmagazine_affiliate>
<!-- the title of our section in the sidebar -->
<label>Affiliate Tracking</label>
<!-- the tab under which we want our section to appear -->
<tab>smashingmagazine</tab>
<!-- order of section relative to our tab -->
<sort_order>10</sort_order>
<!-- visibility of our section -->
<show_in_default>1</show_in_default>
<show_in_website>1</show_in_website>
<show_in_store>1</show_in_store>
<!-- we are adding some new groups to our section -->
<groups>
<!-- the unique short code for our group -->
<general>
<!-- the title of our group -->
<label>General Settings</label>
<!-- order of group relative to the section -->
<sort_order>10</sort_order>
<!-- visibility of our group -->
<show_in_default>1</show_in_default>
<show_in_website>1</show_in_website>
<show_in_store>1</show_in_store>
<!-- we are adding some new fields to our group -->
<fields>
<!-- the unique short code for our field -->
<status>
<!-- the label of our field -->
<label>Enabled</label>
<!-- the type of our field -->
<frontend_type>select</frontend_type>
<!-- the source of our 'select' type -->
<source_model>
adminhtml/system_config_source_enabledisable
</source_model>
<!-- order relative to the group -->
<sort_order>10</sort_order>
<!-- visibility of our field -->
<show_in_default>1</show_in_default>
<show_in_website>1</show_in_website>
<show_in_store>0</show_in_store>
</status>
<merchant_id>
<label>Merchant ID</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>
</merchant_id>
</fields>
</general>
<cookie>
<label>Cookie Settings</label>
<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>
<fields>
<timeout>
<label>Cookie Timeout</label>
<frontend_type>text</frontend_type>
<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>
<!--
we can add a comment that
will appear below the field
-->
<comment><![CDATA[
This is the amount of time
an affiliate cookie will last, in days
]]></comment>
</timeout>
</fields>
</cookie>
</groups>
</smashingmagazine_affiliate>
</sections>
</config>
Defining Default Admin Panel Settings
Sometimes it is convenient to provide default values for your admin panel configuration settings. For example, it would be a good idea to provide a default value for the cookie
timeout value. However, there may be no benefit in providing a default value for merchant_id
, since it will always be different. Let’s update our config.xml
with a default cookie
timeout value:
<?xml version="1.0" encoding="UTF-8"?>
<config>
<modules>
<SmashingMagazine_Affiliate>
<version>0.0.1</version>
</SmashingMagazine_Affiliate>
</modules>
<global>
<blocks>
<smashingmagazine_affiliate>
<class>SmashingMagazine_Affiliate_Block</class>
</smashingmagazine_affiliate>
</blocks>
<models>
<smashingmagazine_affiliate>
<class>SmashingMagazine_Affiliate_Model</class>
</smashingmagazine_affiliate>
</models>
<events>
<controller_front_init_before>
<observers>
<smashingmagazine_affiliate>
<class>smashingmagazine_affiliate/observer</class>
<method>captureReferral</method>
<type>singleton</type>
</smashingmagazine_affiliate >
</observers>
</controller_front_init_before>
</events>
</global>
<frontend>
<layout>
<updates>
<smashingmagazine_affiliate
module="SmashingMagazine_Affiliate">
<file>smashingmagazine_affiliate.xml</file>
</smashingmagazine_affiliate>
</updates>
</layout>
</frontend>
<!-- we are setting the default value -->
<default>
<!-- this is the section short name -->
<smashingmagazine_affiliate>
<!-- this is the group short name -->
<cookie>
<!-- this is the field short name -->
<timeout>30</timeout>
</cookie>
</smashingmagazine_affiliate>
</default>
</config>
Accessing The System Configuration Values
Once all of the above is configured, and we have our default values in place, or have saved our settings through the admin panel, we can easily access these values with the following snippet:
<?php $value = Mage::getStoreConfig('system/config/path'); ?>
The system/config/path
should be replaced with the configuration path to the particular setting you are interested in. For example, the path to our merchant_id
setting would be smashingmagazine_affiliate/general/merchant_id
.
Retrieving Our Cookie Timeout From Admin Panel
We previously had the cookie
lifetime hard-coded to 30 days, which we can now replace with our Magento admin panel system configuration value:
<?php
class SmashingMagazine_Affiliate_Model_Observer
{
const COOKIE_KEY_SOURCE = 'smashingmagazine_affiliate_source';
public function captureReferral(Varien_Event_Observer $observer)
{
$frontController = $observer->getEvent()->getFront();
$utmSource = $frontController->getRequest()
->getParam('utm_source', false);
if ($utmSource) {
Mage::getModel('core/cookie')->set(
self::COOKIE_KEY_SOURCE,
$utmSource,
$this->_getCookieLifetime()
);
}
}
protected function _getCookieLifetime()
{
$days = Mage::getStoreConfig(
'smashingmagazine_affiliate/cookie/timeout'
);
// convert to seconds
return (int)86400 * $days;
}
}
Retrieving Our Merchant ID From Admin Panel
We can now complete our block
by replacing the hard-coded values with calls to the Magento admin panel system configuration. We will also add a new method for retrieving whether the module should be “active” or not:
<?php
class SmashingMagazine_Affiliate_Block_Conversion
extends Mage_Core_Block_Template
{
public function getIsActive()
{
return Mage::getStoreConfig(
'smashingmagazine_affiliate/general/status'
) ? true : false;
}
public function getMerchantId()
{
return Mage::getStoreConfig(
'smashingmagazine_affiliate/general/merchant_id'
);
}
public function getAffiliateId()
{
return Mage::getModel('core/cookie')->get(
SmashingMagazine_Affiliate_Model_Observer::COOKIE_KEY_SOURCE
);
}
}
Enabling Or Disabling Our Module Through The Admin Panel
With this simple modification to our template, conversion.phtml
, we can prevent the image from appearing on the checkout success page if the module status is disabled in the Magento admin panel.
<?php if ($this->getIsActive()): ?>
<img
src="/themes/smashingv4/images/logo.png
?merchant_id=<?php echo $this->getMerchantId() ?>
&affiliate_id=<?php echo $this->getAffiliateId() ?>"
width="1"
height="1"
/>
<?php endif; ?>
Similar if
statements can be added in Observer.php
to prevent the affiliate cookie
being checked for and saved.
Summary
By carefully considering the way we structure our code, and utilizing the Magento admin panel, we can create community modules that are portable enough to simply drop on to any Magento website and they will work out of the box. They can then be customized and fine tuned as required to suit the needs of each website.
Feel free to view or download the source code (Github), and, as always, I welcome any questions and would love to hear any feedback in the comments area below.