How to modify existing extension without fear?

Featured Image

Today I’m going to give you an real life example on how to modify commercial extension in painless manner. If you know how to modify it – great, but is your approach best one? I’ve added functionality and modified behaviour of OSC’s OneStepCheckout extension without any fear of what will happen if client decides to upgrade it to newer version. And finally – here’s how!

First of all, exact thing I had to do is to implement Cdyne’s Address Verification to the checkout over their API. Now, since OneStepCheckout is commercial (and probably upgradeable) I couldn’t just open it’s files and modify them, since all that work would be gone in matter of seconds if client decides to install newer version, so I’ve decided to create an extension of my own that will depend on OneStepCheckout.

That was step 1.
Important things here are:
To do your modification as independent as much as you can.
To keep your work safe as much as you can from overwriting.

Additionally I didn’t want any unnecessary files in templates, as if I overwrite their checkout template file and if they decide to turn off my extension, there’s a big chance that they’ll get some sort of error on frontend, and nobody likes that. So, I’ve used approach that my colleague described here.

That was step 2.
Important thing here is:
Don’t modify any template files in case they aren’t yours.

And now, let me give you a real-life example of what I wrote to apply all of these rules:

Inchoo_Cdyne.xml (from modules folder):

<?xml version="1.0"?>
<config>
    <modules>
        <Inchoo_Cdyne>
            <active>true</active>
            <codePool>local</codePool>
        </Inchoo_Cdyne>
        <depends>
            <Idev_OneStepCheckout/>
        </depends>
    </modules>
</config>

config.xml:

<?xml version="1.0"?>
<config>
    <modules>
        <Inchoo_Cdyne>
            <version>0.1.0</version>
        </Inchoo_Cdyne>
    </modules>
    <global>
        <models>
            <cdyne>
                <class>Inchoo_Cdyne_Model</class>
            </cdyne>
        </models>
        <helpers>
            <cdyne>
                <class>Inchoo_Cdyne_Helper</class>
            </cdyne>
        </helpers>
        <blocks>
            <cdyne>
                <class>Inchoo_Cdyne_Block</class>
            </cdyne>
        </blocks>
        <events>
            <core_block_abstract_to_html_before>
                <observers>
                    <inchoo_cdyne_init>
                        <type>singleton</type>
                        <class>cdyne/cdyne</class>
                        <method>setCdyneMessage</method>
                    </inchoo_cdyne_init>
                </observers>
            </core_block_abstract_to_html_before>
            <sales_order_place_after>
                <observers>
                    <sales_place_observer>
                        <class>cdyne/cdyne</class>
                        <method>checkAddress</method>
                    </sales_place_observer>
                </observers>
            </sales_order_place_after>
        </events>
    </global>
</config>

As you can see, I have 2 connected observers here, and I had to connect them somehow with keeping in mind that they won’t trigger at the same time. For that, I used core/session model.

Since sales_order_place_after observer will trigger before core_block_abstract_to_html_before, some stripped logic looked like this:
Partial Cdyne.php observer:

public function checkAddress($event = null)
	{
		if (!$event) {
			return;
		}
		$request = Mage::app()->getRequest();
 
		//Allowed response codes
		$allowedResponseCodes = array(
			100 => 'Input Address is DPV confirmed for all components',
			101 => 'Input Address is found, but not DPV confirmed',
			102 => 'Input Address Primary number is DPV confirmed. Secondary number is present but not DPV confirmed',
			103 => 'Input address Primary number is DPV confirmed, Secondary number is missing.',
			200 => 'Canadian address on input. Verified on City level only.'
		);
 
		if ($request->isPost()) {
 
			$postAddress = $request->getPost();
			$postAddress = $postAddress['shipping'];
 
			if (is_array($postAddress)) {
 
				//Retrieve data from API
				//$responseCode = 2;
				$responseCode = self::_checkDataWithApi($postAddress);
 
				//Check if response is in allowed codes array
				if (in_array($responseCode, array_keys($allowedResponseCodes))) {
					//Check details and log accordingly
					if ($responseCode > 100) {
						//Log if there's something to warn about
						Mage::log(
								date("Y-m-d h:i:s") .
									' - ' .
									$allowedResponseCodes[$responseCode],
								Zend_Log::WARN,
								'Cdyne_Extension_Warnings.txt',
								true
						);
					}
				} else {
					Mage::log(
							date("Y-m-d h:i:s") .
								' - Invalid License Key',
							Zend_Log::CRIT,
							'Cdyne_Extension_Errors.txt',
							true
					);
				}
 
				//Check if response reports invalid address
				if ($responseCode == 10) {
					$errorValue = Mage::getSingleton('core/session')->getCdyneErrorMessage(self::$errorMessage);
					if (empty($errorValue)) {
						Mage::getSingleton('core/session')->setCdyneErrorMessage(self::$errorMessage);
					}
 
					header('Location: ' .Mage::getUrl('onestepcheckout/'));
					exit;
				} else {
					Mage::register('cdyneErrorMessage','',true);
				}
 
				if (self::$testing) {
					Zend_Debug::dump('Response code: ' . $responseCode);
					exit;
				}
			} else {
				$postVars = var_export($request->getPost(), true);
				Mage::log(
						'Problem with POST values parsing.
						POST variables:
						' . $postVars,
						Zend_Log::WARN,
						'Cdyne_Extension' . date("Y-m-d-h-i-s") . '.txt',
						true
				);
			}
		}
	}
 
/*
Inject message into OneStepCheckout extension
*/
public function setCdyneMessage($event)
	{
		if (!$event) {
			return;
		}
 
		if('onestepcheckout.checkout' == $event->getEvent()->getBlock()->getNameInLayout()) {
			if (!Mage::getStoreConfig('advanced/modules_disable_output/'.self::MODULE_NAME)) {
				$errorValue = Mage::getSingleton('core/session')->getCdyneErrorMessage();
				if (!empty($errorValue)) {
					$event->getEvent()->getBlock()->formErrors['unknown_source_error'] = $errorValue;
					Mage::getSingleton('core/session')->setCdyneErrorMessage(null);
				}
			}
		}
	}

If you look at this 2 lines:

if('onestepcheckout.checkout' == $event->getEvent()->getBlock()->getNameInLayout()) {
//AND
$event->getEvent()->getBlock()->formErrors['unknown_source_error'] = $errorValue;

you can notice that I injected my custom message directly into extension’s error block.

I hope you got the grasp of what I did here, and that I helped someone learn something.

If you’re interested, you can download the extension here, but keep in mind that using it will be at your own risk, and make sure you create a backup before.

Cheers!


5 comments

  1. Any idea if you could use USPS API instead of Cydne?
    We are looking for a way to alert customers at checkout if their shipping address contains errors and give them a chance to correct any errors before completing the order.

    Have you seen anything like this?

  2. I think i follow the code! can i ask a question about hooking into core_block_abstract_to_html_before? i’ve tried to use this as a more generic way of setting cache lifetimes but i found performance was really bad as the number of callbacks i received was huge – how have you found it?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.