Posts Tagged ‘magento’

Solving the IE 7, IE 9 Magento & Prototype Validation Bug

UPDATE: Read this first please…

After receiving some comments from fellow web devs [see comments between 10/26 and 11/11], I decided to integrate some of their changes. I’ve also included some enhancements via my colleague, Tom Rosario. They include a couple of updated variables (hopefully eliminating any headaches with the original ‘fieldsFilled’ infinite increment issue) and a regEx that validates the the email field, once (or if) the core validation fails.

For the rest of you…read the original post [below] to gain a little background/perspective and see if this suits your current issue. Feel free to challenge me and post fixes/improvements (We’re all fighting the same [grunt, groan, teeth-gnash] IE-quirks-battle after all ;) I will also be in the process of trying to find inconsistencies and will update accordingly if I find them.

[original post below]

Hey there fellow Magento-ers,

Have you been spending countless hours scouring the internet for a solution to this odd problem, in Magento, where you use the built-in Prototype validation to validate your form and it does not? And by ‘does not [validate]‘ I mean that in Internet Explorer 7 AND 9 (of course) it manages to ‘validate’ your empty fields, when attempting to submit an empty form, and then immediately disregards this ‘validation’ and just submits the form anyway….

Annoying, right? Can’t seem to get around it, right?

Well, have no fear! I’ve already ruined my own life, productivity and sanity in order to bring you this very specific and (I think) simple solution. I wrote (with the help of my pal Tom Rosario) a little unobtrusive Class-based function to handle it!

Just to give you some background, and to ensure I’ve been as thorough as possible, I’d like to list the things I’ve already attempted and failed at. This includes both my own solutions and those suggested by experts on the web:

  • Stopping the page from loading via the location and window object: simply doesn’t work
  • Changing the version of prototype: messes up all kinds of things in Magento, namely, checkout…so don’t do it
  • Using jQuery validation: same issue occurs
  • Using the IE 7/IE 8 compatibility meta tag: should work, but I never had success with it

Now that we’ve covered some of my futile attempts, let me tell you what I know worked; putting a good, old-fashioned ‘ onsubmit=”return false” ‘ on the form itself. This, of course, is only half the work. You then need to remove this, once the validation is satisfied and only if all the required fields are filled. In my humble opinion, this is (currently) the best solution because we’re allowing the built-in validation to still do it’s thing, with an extra layer of validation to prevent any unwanted results. The best part of this is that it doesn’t negatively affect functionality in the other browsers (FF, Chrome, Safari), who weren’t experiencing this problem in the first place. [raised eyebrow]

So speaking of that extra layer, the approach is really quite simple; we prevent the form from submitting anything if all the fields with the ‘required’ classes are empty, then remove that constraint once they’re filled/validated. Prototype validation is still able to do it’s thing, and when it’s done, and all other aspects are satisfied, we go ahead and take the ‘return false’ off the form and let it submit. Pretty simple right? I’m surprised myself that it was that easy.

Here’s the Class:

/* FormValidator Fix for IE7 & IE9 */

var ie7 = $j.browser.msie && $j.browser.version=="7.0";
var ie9 = $j.browser.msie && $j.browser.version=="9.0";
var safari =$j.browser.safari;
/*--------------------------------------------------------------------------*/

var FormValidator = function (form, opts) {
	this.form = $j(form);
	this.required = form.find('.required-entry');
	this.defaults = {
		email: form.find('.validate-email')
	};
	this.options = $j.extend({}, this.defaults, opts);
	this.setup();
};
FormValidator.prototype = {
	setup: function () {
		var self = this;

		this.form.submit(function () {
			self.onSubmit();
		});
	},
	validateEmail: function () {
		var emailCheck = this.options.email.val().match(/^([a-z0-9,!\#\$%&'\*\+\/=\?\^_`\{\|\}~-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z0-9,!\#\$%&'\*\+\/=\?\^_`\{\|\}~-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*@([a-z0-9-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z0-9-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*\.(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]){2,})$/i);
		if (emailCheck != null) { return true; }
		else { return false; }
	},
	onSubmit: function () {
		var self = this, numFilled = 0;
		this.required.each(function (idx) {
			if ($j(this).val().length > 0) {
				numFilled++;
			}
		});

		var valid = this.validateEmail() && (numFilled == this.required.length);
		if (valid) {
			this.form.removeAttr('onsubmit');
			this.form.submit();
			return true;
		} else {
		/*uncomment the alert below to test your numFilled variable
		if all is going well it shouldn't increment and match the
		number of required inputs you have*/
		//alert(numFilled);

		}
	}
};

Here’s the Call (put this below your form markup):

<script type="text/javascript">// <![CDATA[
	var contactForm = new VarienForm('myform', true);
if (ie7 || ie9 || safari) {
    $j('#myForm').submit(function(e){
        if ($j('#myForm').find('.required-entry:input[value=""]').length <= 0 ) { this.submit(); }
    });
    new FormValidator( $j('#myForm') );
} else {
    // remove the 'onsubmit' and don't run the function, as all non-IE browsers don't seem to have this bug
    $j('#myForm').removeAttr('onsubmit');
}
// ]]></script>

It’s really that simple kids. Please feel free to add a comment and let me know if this didn’t work for you, or if you’d like to add something.

Technology

Creating a Stateless Request in Magento

Have you ever wanted to create a stateless request in Magento? Something that doesn’t touch any of Magento’s sessions?  We were having issues with some of the ajax calls on our cart and checkout pages mucking with the user’s cart and had get stateless on these calls.  The issue we were having was our checkout page was loading, then a javascript include was going out and bringing code from a 3rd party relevance engine into our dom, which was in turn calling back an ajax request to our servers.  This issue with this being that at the start of the page load, the checkout session was being set to a certain state.  This state was then being sent through the rest of the page load, and the ajax calls. Unfortunately, by the time the ajax call got back to our server, the session was different in both locations, creating a race condition.  The ajax request usually won, removing the work the full page load had done with trying to process checkout.  The good news was there was nothing in the ajax call that needed to touch the session, it was just some data lookup. So, nix the session part of that call, and our troubles should be over… Magento’s api controller is the only place that implements a stateless request this but its fairly easy to do (after a bit of digging).

As long as Mage_Core_Controller_Varien_Action is a parent in your controller’s hierchy, you are good to go (it probably is).  This class has a const FLAG_NO_START_SESSION which looks promising. Digging into the code a little we see that it controls whether cookies are processed or the session is started:

<?php
...
        if (!$this->getFlag('', self::FLAG_NO_START_SESSION)) {
            $checkCookie = in_array($this->getRequest()->getActionName(), $this->_cookieCheckActions);
            $checkCookie = $checkCookie && !$this->getRequest()->getParam('nocookie', false);
            $cookies = Mage::getSingleton('core/cookie')->get();
            if ($checkCookie && empty($cookies)) {
                $this->setFlag('', self::FLAG_NO_COOKIES_REDIRECT, true);
            }
            Mage::getSingleton('core/session', array('name' => $this->_sessionNamespace))->start();
        }
view raw gistfile1.aw This Gist brought to you by GitHub.

By adding to the preDispatch() method of our Action or Controller we can toggle this:

<?php
class Ai_AjaxCatalog_Controller_Action extends Mage_Core_Controller_Front_Action
{
public function preDispatch()
{
$this->setFlag('', self::FLAG_NO_START_SESSION, 1); // Do not start standard session
parent::preDispatch();
return $this;
}
}
view raw gistfile1.aw This Gist brought to you by GitHub.

Now, any action in this controller will be stateless and not effect any sessions.

Technology

EBay Acquires Magento

Ebay today announced they are acquiring everyone’s favorite ecom platform Magento.  They already owned 49% of the platform and have announced plans to roll Magento into some new X.Commerce initiative.  This is a very smart move for EBay.  Have recently acquired GSI to offer an enterprise solution at the very top of the online retail food chain, EBay can even more effectively compete at the entry- and mid-levels of ecommerce.

We’re huge fans of Magento here at Ai, and will be watching this development closely.  On the one hand, the additional engineering resources, marketing, product stewardship, and enterprise support will be welcomed by ecom brands and developers alike.  Graduating from a 49% strategic investment to a fully-blown integrated product suite should come with the commensurate level of attention from EBay execs.

On the other hand, all too often we’ve seen thriving software platforms gobbled up by larger companies primarily with the intent of folding the acquired company’s customers into the acquirer’s existing product suite.  This may not be welcome news for us Magento devs out there that enjoy direct access to the source code of the product and significant engineering accumen and performance tuning experience on the platform.

We will certainly keep our eyes on this and report back to our friends and clients any important implications.  In the meantime, congratulations to the folks at Magento and founder Roy Rubin.

Ecommerce

Extending a Magento Controller

We’re ajaxing part of the Magento shopping cart so we need to modify/extend some of the cart controller functionality. Sometimes when modifying controller’s you have to worry about updating the routes. For this, we don’t need to, we still want all the urls to be used the same way.

app/code/local/Ai/Checkout/etc/config.xml:

<config>
    <modules>
        <Ai_Checkout>
             <version>0.0.1</version>
        </Ai_Checkout>
    </modules>
...
    <frontend>
        <routers>
            <checkout>
                <use>standard</use>
                <args>
                    <module>Ai_Checkout</module>
                    <frontName>checkout</frontName>
                </args>
            </checkout>
        </routers>
    </frontend>
</config>

app/code/local/Ai/Checkout/controllers/CartController.php:

require_once Mage::getModuleDir('controllers', 'Mage_Checkout') . DS . 'CartController.php';

class Ai_Checkout_CartController extends Mage_Checkout_CartController
{
   public function updatePostAction()
    {
    	Mage::log("NEW CONTROLLER", null, 'tim.log');
        try {
Technology

Want to trace the call stack in Magento?

This has helped me immensely in situations like “Where is this getting called from??!?”

Create a helper like so:

class Timbroder_Stack_Helper_Callstack extends Mage_Core_Helper_Abstract
{
	private function get_callstack($delim="\n") {
	  $dt = debug_backtrace();
	  $cs = '';
	  foreach ($dt as $t) {
	    $cs .= $t['file'] . ' line ' . $t['line'] . ' calls ' . $t['function'] . "()" . $delim;
	  }

	  return $cs;
	}

	public function toLog() {
		Mage::log($this->get_callstack());
	}

	public function toFirePhp() {
		$stack = $this->get_callstack();
		foreach (explode("\n", $stack) as $line) {
			Mage::helper('firephp')->send($line);
		}
	}
}

That can be called from anywhere:

Mage::helper('stack/callstack')->toFirePhp();
Mage::helper('stack/callstack')->toLog();

I’ve also wrapped this into a module that you can drop right into your project. Details here

Example output:

.../app/code/community/Timbroder/Stack/Helper/Callstack.php line 16 calls get_callstack()
.../app/design/frontend/mongoose/default/template/catalog/cms/bikes_bmx.phtml line 12 calls toLog()
.../app/design/frontend/mongoose/default/template/catalog/cms/bikes.phtml line 21 calls require_once()
.../app/code/core/Mage/Core/Block/Template.php line 212 calls include()
.../app/code/core/Mage/Core/Block/Template.php line 239 calls fetchView()
.../app/code/core/Mage/Core/Block/Template.php line 253 calls renderView()
.../app/code/core/Mage/Core/Block/Abstract.php line 668 calls _toHtml()
.../app/code/core/Mage/Core/Model/Email/Template/Filter.php line 190 calls toHtml()
.../lib/Varien/Filter/Template.php line 134 calls call_user_func()
.../app/code/core/Mage/Core/Model/Email/Template/Filter.php line 501 calls filter()
.../app/code/core/Mage/Cms/Block/Page.php line 100 calls filter()
.../app/code/core/Mage/Core/Block/Abstract.php line 668 calls _toHtml()
.../app/code/core/Mage/Core/Block/Abstract.php line 513 calls toHtml()
.../app/code/core/Mage/Core/Block/Abstract.php line 460 calls _getChildHtml()
.../app/code/local/Mage/Page/Block/Html/Wrapper.php line 52 calls getChildHtml()
.../app/code/core/Mage/Core/Block/Abstract.php line 668 calls _toHtml()
.../app/code/core/Mage/Core/Block/Text/List.php line 43 calls toHtml()
.../app/code/core/Mage/Core/Block/Abstract.php line 668 calls _toHtml()
.../app/code/core/Mage/Core/Block/Abstract.php line 513 calls toHtml()
.../app/code/core/Mage/Core/Block/Abstract.php line 464 calls _getChildHtml()
.../app/design/frontend/mongoose/default/template/page/1column.phtml line 55 calls getChildHtml()
.../app/code/core/Mage/Core/Block/Template.php line 212 calls include()
.../app/code/core/Mage/Core/Block/Template.php line 239 calls fetchView()
.../app/code/core/Mage/Core/Block/Template.php line 253 calls renderView()
.../app/code/core/Mage/Core/Block/Abstract.php line 668 calls _toHtml()
.../app/code/core/Mage/Core/Model/Layout.php line 529 calls toHtml()
.../app/code/local/Mage/Core/Controller/Varien/Action.php line 389 calls getOutput()
.../app/code/core/Mage/Cms/Helper/Page.php line 130 calls renderLayout()
.../app/code/core/Mage/Cms/Helper/Page.php line 52 calls _renderPage()
.../app/code/core/Mage/Cms/controllers/PageController.php line 45 calls renderPage()
.../app/code/local/Mage/Core/Controller/Varien/Action.php line 418 calls viewAction()
.../app/code/core/Mage/Core/Controller/Varien/Router/Standard.php line 254 calls dispatch()
.../app/code/core/Mage/Core/Controller/Varien/Front.php line 177 calls match()
.../app/code/core/Mage/Core/Model/App.php line 304 calls dispatch()
.../app/Mage.php line 598 calls run()
.../index.php line 155 calls run()

Thanks to nextide for some of the code

Technology

Using widgets outside of the CMS in Magento

Magento ships with widget functionality that lets you build out data models and then reuse them on product and CMS pages. If you want to use these in a custom template however, you are out of luck. This can be done by extending the Widget Collection class.

Create the following directory structure: app/code/local/Mage/Widget/Model/Myswql4/Widget/Instance

Copy app/code/core/Mage/Widget/Model/Myswql4/Widget/Instance/Collection.php into your new directory

The Mage_Widget_Model_Mysql4_Widget_Instance_Collection comes with a store filter but thats about it. To be more usefull we are going to add a type filter, a title filter, and a sorter.

class Mage_Widget_Model_Mysql4_Widget_Instance_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract
{
    /**
     * Constructor
     */
    protected function _construct()
    {
        parent::_construct();
        $this->_init('widget/widget_instance');
    }

    /**
     * Filter by store ids
     *
     * @param array|integer $storeIds
     * @param boolean $withDefaultStore if TRUE also filter by store id '0'
     * @return Mage_Widget_Model_Mysql4_Widget_Instance_Collection
     */
    public function addStoreFilter($storeIds = array(), $withDefaultStore = true)
    {
        if (!is_array($storeIds)) {
            $storeIds = array($storeIds);
        }
        if ($withDefaultStore && !in_array(0, $storeIds)) {
            array_unshift($storeIds, 0);
        }
        $select = $this->getSelect();
        foreach ($storeIds as $storeId) {
            $select->orWhere('FIND_IN_SET(?, `store_ids`)', $storeId);
        }
        return $this;
    }

    public function addTypeFilter($type) {
    	$this->getSelect()->where('type=?', $type);
    	return $this;
    }

    public function addTitleFilter($type) {
    	$this->getSelect()->where('title=?', $type);
    	return $this;
    }

    public function addAttributeToSort($attribute, $dir='asc') {
    	$this->getSelect()->order("{$attribute} {$dir}");
    	return $this;
    }
}

Now we should be able to query any widgets from any template in our system:

< ?php
$wids = Mage::getModel('widget/widget_instance')
	->getCollection()
	->addTypeFilter('masswidget/list')
	->addAttributeToSort('title', 'asc')
	->load();

foreach ($wids as $wid):
	$params = $wid->getWidgetParameters();
	echo $params['custom_param'];
	echo $wid->gettitle();
endforeach;
?>
Technology

Improving the Performance of a Local Magento Install

Magento is great, but it needs a good amount of hardware behind it.  Developing locally can get slow and cumbersome unless your environment is tweaked properly.  Here are a few tips for boosting Magento performance without impacting the rest of your development environment.  Please keep in mind that the memory allocations work well for my machine (dual core, 4 gigs of ram).

Database
Install innoDB.  Magento can use the in-memory buffer pool to cache table indexes and data.

Configure my.ini:
innodb_buffer_pool_size  = 64M
innodb_thread_concurrency = 4 (or 8 if you have dual core)
query_cache_size = 64M
query_cache_limit  = 2M

apache
enable mod_expires in httpd.conf

php
in php.ini enable:
realpath_cache_size = 16k
realpath_cache_ttl = 120

Install the eAccelerator binaries for php.  APC is a better solution but is less compatible with windows.  If you need to compile these, click here for instructions. Then configure it:
extension=eaccelerator.dll
eaccelerator.shm_size=64
eaccelerator.cache_dir=C:\PHP\tmp
eaccelerator.enable=1
eaccelerator.optimizer=1
eaccelerator.check_mtime=1
eaccelerator.shm_max=0

Install memcached.
add the following lines inside the config of epp/etc/local.xml
<cache>
    <backend>memcached</backend>
    <memcached>
        <servers>
            <server>
                <host><![CDATA[localhost]]></host>
                <port><![CDATA[11211]]></port>
                <persistent><![CDATA[1]]></persistent>
            </server>
        </servers>
        <compression><![CDATA[0]]></compression>
        <cache_dir><![CDATA[]]></cache_dir>
        <hashed_directory_level><![CDATA[]]></hashed_directory_level>
        <hashed_directory_umask><![CDATA[]]></hashed_directory_umask>
        <file_name_prefix><![CDATA[]]></file_name_prefix>
    </memcached>
</cache>

Admin Backend

  • Keep the indexes up to date (System > index management)
  • Compile Mage classes (System > tools > Complilation)
  • Enable all cachine (System > Cache Management)
Technology