We are developing a site which makes use of Commerce Multicurrency, but we need to be able to cache pages for anonymous users. As things stand, users will be presented with the cached page, regardless of which currency is set in the commerce_currency cookie. Is there a way to cache pages for each currency and return the appropriate page based on the value of commerce_currency?

Comments

stefok’s picture

i had same problem, and solved it with another module http://drupal.org/project/cache_actions (maybe not the best solution but better than site without enabled cache).

Create new rule with name like "clear cache after select currency"

Event -> After the user currency has set
Actions -> Clear cache bins (i have block + page)

Hope it will help ;)

hyperglide’s picture

Hello -- we have a similar issue with currency and caching.

In conversation with one developer not familiar with commerce_multi_currency suggested reloading the price field(s) for the conversions with AJAX.

Thoughts?

michaellenahan’s picture

@stefok: thank you very much!

The exact solution you suggest works perfectly for me.

Some more detail, to make this issue more discoverable via Google, and a bit more newbie-friendly:

For anonymous users, the currency switcher drop-down was having no effect because "cache pages for anonymous users" was set in admin/config/development/performance.

Logged-in users were able to change currency without any problem.

The cache_actions module allows us to define a rule which will clear the cache after certain actions have occurred on the site.

I followed the instructions in #1 and it solved the problem for me.

Prague man’s picture

@stefok: great issue, thank you!

stefok’s picture

NP, i am glad to share working solution. But remember, this is just first aid and imho not usable on sites with thousands visitors per day, because it clear cache every time when non default currency is selected.

pokadan’s picture

use cacheexclude to exclude specific pages?

das-peter’s picture

@poka_dan Good idea :) Using CacheExclude to exclude pages, where prices are generated, from being cached in the page cache sounds like a viable approach.

mfgering’s picture

How about another solution: modify the currency links to include a query parameter that should defeat a caching system?

The multi-currency module theme template creates currency links of the form commerce_currency_select/curr-code?destination=dest, for example: commerce_currency_select/AUD?destination=node/423 The currency module uses the destination parameter to redirect after changing the user currency. If the link is modified to include an embedded query parameter with a random or unique value, e.g. a timestamp, e.g. ?time=123456, the redirect will include it and (hopefully) defeat the caches. This parameter must be url-encoded, e.g. commerce_currency_select/AUD?destination=node/423%3Ftime%3D1361365080.

It appears to work. A slight modification to the multi-currency module's template, commerce-multicurrency-selector-menu.tpl.php seems to do the trick. I've tested this with a template override for our theme. But I think it might be a good option to include with the module, perhaps controlled by a module setting.

<?php
/**
 * Available variables:
 * $enabled_currencies
 * $user_currency
 */
  //Note: We modify the template here to introduce a 'time' query parameter. This is intended to defeat page caching for the
  // destination page (since the time parameter will always be different). The time parameter is encoded within the destination url.
  $dest = drupal_get_destination(); // get current destination array
  $options = drupal_parse_url($dest['destination']); // parse the destination url
  $path = $options['path']; // save original destination path
  $options['query']['time'] = time(); // add or update the time query parm
  $path .= (strpos($path, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($options['query']); //rebuild the destination url
  $destination = array('destination' => $path); // build new destination array
?>
<ul class="currency_select_menu">
<?php foreach($enabled_currencies as $currency) : 
?>
  <li class="<?php print $currency['code'] . (($currency['code'] == $user_currency) ? ' active' : NULL); ?>">
    <a href="<?php print url('commerce_currency_select/' . $currency['code'], array('query' =>  $destination)); ?>" rel="nofollow"><?php print $currency['code']; ?></a>
  </li>
<?php endforeach;?>
</ul>

das-peter’s picture

@mfgering: The effect will be gone as soon as you change the page using another link, as you'll loose the "random" url parameters. So that isn't a very sustainable approach.
To be sustainable you would need to implement something like the language path-prefix / sub-domain.

mfgering’s picture

@das-peter: Thanks for pointing that out. The use case this satisfies is where the currency select form and the relevant prices are on the same page.

However, now I'm thinking even in that case a better solution might be to use the new currency code in the query parameter instead of a random or unique value. This way, the cached pages are indexed by the selected currency; ultimately there could be X versions of a given page in the cache, each with a different currency selection.

I'd like to understand your suggested approach. Do you mean we could encode the currency selection in the domain name, e.g. aud.foo.com and eur.foo.com?

das-peter’s picture

I'd like to understand your suggested approach. Do you mean we could encode the currency selection in the domain name, e.g. aud.foo.com and eur.foo.com?

I don't know if this is a viable way, it just came to my mind because of the similarity between currency codes and language codes. So if you plan to do something like that checkout how the language prefix stuff works and maybe you're able to reproduce that for currencies as well.

codekarate’s picture

Here is my approach which appears to be working. It is just an additional function added to the commerce_multicurrency module that redirects the user based on the currency they have selected by adding a query string parameter to the URL. This was a quick fix and I know it will not work correctly if your site makes use of query strings in a lot of other places (potentially with things like views filters, etc). It can most definitely be improved upon and I welcome any suggestions to make it better. Note: It also will most likely not work if your Drupal site is in a subdirectory.

I just added this to the commerce_multicurrency.module file:

/**
 * Implements hook_boot().
 */
function commerce_multicurrency_boot() {
  // If this is an ajax request, we return early.
  if (stripos($_GET['q'], 'system/ajax') === 0) {
    return;
  }
  
  // Otherwise we check the currency of the current user.
  drupal_load('module', 'commerce');
  
  $default_currency = commerce_default_currency();
  $currency_code = $default_currency;
  
  // If there's a cookie with a selected currency ensure it's a available one.
  if (isset($_COOKIE['Drupal_visitor_commerce_currency'])) {
    $currency_code = $_COOKIE['Drupal_visitor_commerce_currency'];
  }

  if ($currency_code != $default_currency && (!isset($_GET['currency']) || $_GET['currency'] != $currency_code)) {
    $current_url = $_GET['q'];
    
    if (stripos($current_url, '?') && !stripos($current_url, 'currency=')) {
      $redirect = $current_url . '&currency=' . $currency_code;
    }
    else {
      $redirect = $current_url . '?currency=' . $currency_code;
    }
    
    // Even though session_write_close() is registered as a shutdown function, we
    // need all session data written to the database before redirecting.
    session_write_close();
    
    header('Location: /' . $redirect, TRUE, 301);
  
    // The "Location" header sends a redirect status code to the HTTP daemon. In
    // some cases this can be wrong, so we make sure none of the code below the
    // drupal_goto() call gets executed upon redirection.
    exit();
  }
  elseif ($currency_code == $default_currency && isset($_GET['currency'])) {  
    $path = explode('?', $_GET['q']);
    
    // Even though session_write_close() is registered as a shutdown function, we
    // need all session data written to the database before redirecting.
    session_write_close();
  
    header('Location: /' . $path[0], TRUE, 301);
    exit();
  }
}
luksak’s picture

Is there no way of altering the page cache and make it sensitive to the currency cookie?

As a temporary solution I will be using the CacheExclude approach. @poka_dan Thank you for pointing that out.

luksak’s picture

Version: 7.x-1.1 » 7.x-1.x-dev

I spent some time tying to figure out how to solve this. First I tried the approach described here: http://drupal.org/node/361832#comment-4204294 But it appears that this is not working in D7 anymore.

Then I tried to use this hook_url_inbound_alter() but I was always being redirected as soon I changed the path.

The only reliable solution I came up with was hacking drupal_page_set_cache() and drupal_page_get_cache() by appending the currency code to the cid.

What I am really confused about is that I couldn't find a way to alter the path or cid. Am I missing something?

rphillipsfeynman’s picture

#1 worked perfectly for me. Thanks

rphillipsfeynman’s picture

#1 worked perfectly for me. Thanks

heyyo’s picture

The solution in #12 works great and cache every pages without clearing anything when switching currency
The only thing I changed is the test on the ajax call , which didn't work on my multilang website where all urls are prefixed with lang :

 if (stripos($_GET['q'], 'system/ajax') === 0) {
    return;
  }

Replaced by:

 if (stripos($_GET['q'], 'system/ajax') !== FALSE) {
    return;
  }

I'm not sure what the performance impact to call hook_boot on each page.

I suppose we could also add a condition to return right away if the user is logged in, or in admin section.

luksak’s picture

Yes, #12 works but it is not a solution to the problem to redirect every single request to a URL with appended currency code. We have a caching problem. So we should try to solve the problems with caching and not just make it happen with a ugly workaround.

das-peter’s picture

What I am really confused about is that I couldn't find a way to alter the path or cid. Am I missing something?

That's indeed an issue and actually one of the core hacks I apply to all our customers installations. We've often the need to build complex caching mechanisms and thus we need to alter the page cache cid. The proof of concept code/module is actually available here: https://github.com/das-peter/enhanced_page_cache
It allows you to exclude certain pages from page cache based on the path, too.

luksak’s picture

Thank you for that statement. The only way to get this working is to hack core?

Should we provide a patch with the module to ease the installation? Or a note in the readme.txt how to do it and where to get the patch?

Seriously... why is there no hook that allows us to alter anything? Is there a chance of getting one into core? Is there an issue report for this?

das-peter’s picture

The only way to get this working is to hack core?

If "this" is referring to changing the page cache cid -> yes. Otherwise -> no, the idea of having a currency path prefix, similar to the language path prefix, still seems to be a possible approach to me (#9).

Should we provide a patch with the module to ease the installation?

No I don't think so.

Or a note in the readme.txt how to do it and where to get the patch?

That's more likely.

Seriously... why is there no hook that allows us to alter anything? Is there a chance of getting one into core? Is there an issue report for this?

I'm not joking, but feel free to check the code - it's open source :P
Well, I'm not convinced at all that such a change would made it into core. So as far as I remember I didn't even try. What means: no there's no such issue report - at least none from me.

heyyo’s picture

luksak’s picture

If "this" is referring to changing the page cache cid -> yes. Otherwise -> no, the idea of having a currency path prefix, similar to the language path prefix, still seems to be a possible approach to me (#9).

Just for my case I am going to hack core rather than adding a basically unnecessary parameter to my URLs. But maybe we should provide an option for this as a temporary solution?

That's more likely.

And show here the two options.

I'm not joking, but feel free to check the code - it's open source :P
Well, I'm not convinced at all that such a change would made it into core. So as far as I remember I didn't even try. What means: no there's no such issue report - at least none from me.

Well, I did look at the code and that was my conclusion. I just wanted it to be confirmed by someone else ;)

@heyyo Thank you for pointing that out. Sadly I think I cant really help in that discussion.

luksak’s picture

I applied your patch and wrote the following code in my custom module to make this happen:

<?php
/**
 * Implements hook_page_cache_cid_alter().
 * @param string $cache_key
 */
function hook_page_cache_cid_alter(&$cid) {
  if (!stripos($cid, '|currency=')) {
    if (isset($_COOKIE['Drupal_visitor_commerce_currency'])) {
      $cid .= '|currency=' . $_COOKIE['Drupal_visitor_commerce_currency'];
    }
  }
}

/**
 * Implements hook_page_cache_object_alter().
 * @param stdClass $cache
 */
function hook_page_cache_object_alter(&$cache) {
  // Make sure the same changes to the cid apply here.
  edition_patrick_frey_page_cache_cid_alter($cache->cid);
}

/**
 * Implements hook_boot().
 */
function hook_boot() {
  // Just make sure the module is loaded for boot and the API is available.
}
?>

What do you guys think? Seems pretty decent to me, except hacking core...

luksak’s picture

@das-peter The condition if (!stripos($cid, '|currency=')) is there because i had double parameters at the end of the cid. Any idea why the hook is being called twice?

andrea.brogi’s picture

#1 work for me.

tamnv’s picture

Issue summary: View changes

#1 work for me. Thanks!

szeidler’s picture

Isn't #1 an approach, that creates a lot of side-effects, when two or more people using the currency switcher at the same time? If your commerce installation is dependent on the anonymous users currency selection and not just for displaying the price it would create a lot of confusion. Or am I completely wrong?

jantoine’s picture

Thanks @das-peter and @Lukas von Blarer! #19 and #24 work great! One thing I had to do was manually update the system table setting the bootstrap column to 1 for my module as I was altering an existing module. See https://www.drupal.org/node/1958132#comment-7302018 for an explanation.

das-peter’s picture

@jantoine Hmm, I thought if your module implements hook_boot() the module is registered automatically as "boot" module (see _system_update_bootstrap_status()). Not sure if manually changing that flag in the DB will survive a cache-flush.

n20’s picture

I applied the patch from #19 and enabled the module. In the module settings i added commerce_currency_select/* as excluded from pages to cache. This only works if i set the cache lifetime to 0 in the module settings.

Hope there will be a solution without hacking core..

dr.osd’s picture

#1 works! Thanks to @stefok

vikas_gate6’s picture

#1 works! but some time currency will automatically change. Anybody have any solution or idea why this happen.

dr.osd’s picture

@vikas_gate6 if your currency reset to default, maybe you clean cookies..

vikas_gate6’s picture

@Dr.Osd thanks for reply. It's not reset to default currency it will take any of currency in the drop -down. And when i clear cookies from browser currency reset to default so i don't think cookies clear caused this issue.

Any other solution or idea.

zero4281’s picture

I ran into this same issue and disabling or circumventing the cache is not an option for me. After reading this thread I was left wondering why this module is using a Cookie and not the PHP Session. The Cookie is only referenced in a few places so I patched it in a few minutes. The two functions in question are commerce_multicurrency_set_user_currency_code and commerce_multicurrency_get_user_currency_code, both functions are in commerce_multicurrency.module.

All I had to do in commerce_multicurrency_get_user_currency_code was change $_COOKIE to $_SESSION. In commerce_multicurrency_set_user_currency_code I changed $_COOKIE to $_SESSION and I replaced this:

// Set cookie.
user_cookie_save(array('commerce_currency' => $currency_code));

with this:

// Set session
$_SESSION['Drupal_visitor_commerce_currency'] = $currency_code;

Everything still seems to be in working order for my configuration.

das-peter’s picture

@zero4281 Are you aware that a session uses a cookie too? And that a session with data disables page caching¨?

zero4281’s picture

Thank you for pointing that out! What I actually need is default currency by domain which you just patched into the latest dev. I'll be testing it with Domain Access shortly!

dr.osd’s picture

After some time of using #1 I noticed a serious problem.
#1 works but not good. If you change currency from anonymous user, currency will be changed for all other anonymous users.
This makes #1 unusable.

RAWDESK’s picture

I've applied both Peter's patch and Lukas's custom module implementation but neither one of them or in combination worked for me.
Setting the enhanced page cache page exclusion option to product/* didn't prevent my product pages to be cached also.

Therefore i tried smthomas's approach at #12 and this miraculously seems to work perfectly, even on my faceted search pages (setted up with Search API) with tons of additional added query string parameters.
I have the impression that as long as the ?currency= parameter is putted first in the URL, which happens in my case, the multi currency pages are cached and shown correctly to anonymous users.

sergei_brill’s picture

There another way how to alter the cid - just define your custom cache class where you will be able to generate your own cid. This module https://www.drupal.org/project/cookie_aware_page_cache does it and uses cookie parameter as part of cid. And it doesn't require to alter the drupal core. Let's test it. If it works, will ask commerce_multicurrency maintainer to mention the module in README.txt

roland.molnar’s picture

A good solution - which is working for me - is to use authcache.
In order to make it work, you need to do the following alongside of installing and configuring authcache:

Add the following code to your settings.php

$conf['authcache_key_generator'] = 'MY_key_generator';

function MY_sanitize($input) {
  //@todo: sanitize value by using pre-defined values for example.
  return $input;
}

function MY_key_generator() {
  global $base_root;
  if (isset($_COOKIE['Drupal_visitor_commerce_currency'])) {
    $variant = mju_sanitize($_COOKIE['Drupal_visitor_commerce_currency']);
  }
  else {
    $variant = 'USD'; //@todo: change to your default currency
  }
  return $base_root . '-' . $variant;
}

This will use Commerce Multicurrency cookie and append it to the Authcache key so Authcache will serve different cached pages based on this cookie value (the user's currency).

You need to add your own sanitize function to make sure cache keys will get only allowed values (using an in_array() for example).

Unfortunately, this alone isn't enough, my testing showed that the currency switcher works well until the user adds something to the cart (= Commerce starts a session for the user). After starting a session, when a user changes currency, the current page doesn't change so need to refresh it or navigate to another page in order to see prices in the newly selected currency. I solved this by adding an extra redirect to the submit handler of the selector form:

You should do this in a custom module (replace MODULENAME to your module's name).

/**
* Implements hook_form_FORM_ID_alter().
*/
function MODULENAME_form_commerce_multicurrency_selector_form_alter(&$form, &$form_state, $form_id) {
 $form['#submit'][] = 'MODULENAME_multicurrency_selector_form_submit';
}

/**
* Submit handler for the currency selector block form.
*/
function MODULENAME_multicurrency_selector_form_submit($form, &$form_state) {
 global $user;
 if (isset($user->session)) {
   drupal_goto('commerce_currency_select/' . $form_state['values']['selected_currency'], array('query' => drupal_get_destination()));
 }
}

dr.osd’s picture

Maybe #12 is the simplest solution, but in my case users have been redirected to home page when try to switch currency. (I have multilingual site).
Solution #42 need memcache on server and not be implemented on my hosting.
Solution #41 not easy to understand. @sergei_brill can you give more info please!?

RAWDESK’s picture

@Dr.Osd,
I am using #12 and it's working on my multilingual site without redirection.
Before enabling i18n module i did have to implement a hook_preprocess_html to insert no-caching headers in the html.
see http://stackoverflow.com/questions/49547/making-sure-a-web-page-is-not-c...

It helped me solve a few other issues with multicurrency, especially on commerce cart checkout panes.
Maybe you can give it a try too.

heatoni’s picture

With reference to #41 - the cookie_aware_page_cache module - I've now been using this for a little while to get around the commerce multi-currency caching issue for anonymous users and it seems to be a good solution since it caches and returns the correct variants of the page based on the selected currency cookie.

Getting it working involved adding the cookie "Drupal.visitor.commerce_currency" to the settings.php file as described in the documentation on the module page. I suspect there may be a problem with this when used with the APC cache, but don't know enough to say for sure - if you have problems getting it going, it might be worth playing with the other cache settings and remembering to clear the caches between changes.

You also need to test quite carefully as it's quite easy to convince yourself that it's working if the page you use to test with hasn't been opened before with the cookie set and isn't therefore in the cache - it could appear to work as it will return the right page, but if the page has been opened before and is cached with a different cookie setting you could potentially get the wrong one. (I hit this problem with a different home brew attempt to get around this issue, not the cookie_aware_page_cache, which has been working properly). If you're testing on your local dev environment, you'll need to check that it works with the same cache settings as the live environment too...

I hope that helps.

sergei_brill’s picture

I'm using the Cookie Aware Page Cache module on 2 sites for a few month. It works fine. Also there's a patch for the module which brings supports of Memcached. Looks like using the same way it is possible to add support of any cache backend.

dr.osd’s picture

This problem has haunted me for over a year and no one method does not work. What have I missed?
I tried to use Cookie Aware Page Cache on clear drupal installation, but no success. My settings.php have:
$conf['cache_backends'][] = 'sites/all/modules/cookie_aware_page_cache/CookieAwarePageCache.inc';
$conf['cache_class_cache_page'] = 'CookieAwarePageCache';
$conf['cookie_aware_page_cache_cookies'][] = 'Drupal.visitor.commerce_currency';

danielmrichards’s picture

I agree with the approach suggested in #36. IMO storing the selected currency in a session variable is a more practical solution than installing other contrib modules and/or creating custom page cache rules. Excluding product pages from being cached at all to enable the currency switcher to work is not a great solution especially for sites that experience heavy traffic.

Yes adding a session variable will disable page caching, but only for those users who select a currency and not use the site default. Provided your site is configured with the most popular currency as the default; performance impact should be minimal.

Attached is a patch along these lines.

seemas’s picture

I have made completely new solution that handles currency that is not default one using $_GET.
Possible to force it using "commerce_multicurrency_force_get" new variable:

$conf['commerce_multicurrency_force_get'] = 1;
seemas’s picture

A small fix that contains removing currency from url while user is logged in. This breaks Inline Form Editor ajax requests.

heatoni’s picture

I've just had another struggle with the cookie-aware page cache module to get it working again with Drupal Commerce Multicurrency after it apparently stopped working. I'm not sure why, but the name of the cookie appears differently to PHP within Drupal than it does to Firebug etc. If you use Firebug or Web Developer to see the name of the cookie that is set, it'll be "Drupal.visitor.commerce_currency", but if you stick var_dump($_COOKIE); in the PHP, you'll see that the web server sees the same cookie as "Drupal_visitor_commerce_currency". I don't know how, why or when it gets changed. It appears that it's just been Drupalled and I have had enough of my soul drained by this issue that I just don't want to know.

Anyway, if you're trying to get the cookie-aware page cache module to work with commerce multicurrency, try setting the name of the cookie in settings.php to "Drupal_visitor_commerce_currency" - note the underscores replacing the dots.

It's also worth noting that clearing the page cache doesn't work with the cookie-aware page cache module until you apply the patches mentioned at https://www.drupal.org/node/2730183.

Have a look at the contents of the cache_page table in MySQL if you want to make sure it's working. You should see the cid column showing the name and value of the cookie being prefixed to the page name so that a different cache record is held for each page and cookie combination that has been viewed which is exactly what we want.

dr.osd’s picture

Excellent! Thanks Heatoni #51

alfthecat’s picture

@heatoni in #51,

Thank you! You totally nailed it!

iampuma’s picture

Had the same issue with the currency switcher for anonymous users with page caching enabled, patch #48 works like a charm for me. Thanks.

romstach’s picture

Patch in #48 is perfect.
Thank you

alancooney’s picture

For those still struggling with this, I have attached some files:

- Cache_alter.patch to patch commerce kickstart
- Commerce_multicurrency_geo - Alters the cache by currency (adding |currency=XYZ to the cache id). It also automatically sets the correct currency using GeoLite2-City database (download it to libraries/geoip/GeoLite2-City.mmdb from http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz ) if it is downloaded there.

After installing, clear all caches and you're good to go.