Running multiple Magento stores on multiple domains from a single index.php

For our eCommerce project, we are building multiple websites each with multiple languages, but all managed from one Magento install. We wanted to run each website from it’s own domain, with each store view as a subfolder for the language, such as (The base link urls are configured like this for each store view from the Magento admin panel):

  • http://store1.example.com/en/ (store name store1_en)
  • http://store1.example.com/fr/ (store name store1_fr)
  • http://store1.example.com/de/ (store name store1_de)
  • http://store1.example.com/es/ (store name store1_es)
  • http://store1.example.com/nl/ (store name store1_nl)
  • http://store2.example.com/en/ (store name store2_en)
  • http://store2.example.com/fr/ (store name store2_fr)
  • http://store2.example.com/de/ (store name store2_de)
  • http://store2.example.com/es/ (store name store2_es)
  • http://store2.example.com/nl/ (store name store2_nl)

Looking around the net for solutions, it seems the majority of suggestions are to duplicate the magento index.php and .htaccess into a folder for each language/store view. This would mean more files to maintain, and a more complicated structure. We also did not want to have to rely on .htaccess for the security and url rewriting, but rather maintain the rewrite configuration as well as the store URL to store name mapping in the main Apache config. We also didn’t want to have to extend the folder structure just because we added a new store view/language in the future.

I came up with a solution for this, utilising a slightly modified index.php which “runs” each store, does some querystring modifications, and an Apache configuration to handle the rewriting. This allows us to easily add another store view, just by altering the Apache config.

First of all here is our site structure. the js/media/skin folders are symbolic links to the parent magento folders (that is outside of the web accessible root)

site/
    magento/ (standard magento install)
    store/
          index.php
          js     -> ../magento/js
          media  -> ../magento/media
          skin   -> ../magento/skin
          report -> ../magento/report

Each virtualhost points to the store/ folder. It seemed safer to keep the main magento folder outside of the site root, and everything so far seems to work ok like this. Note there is no .htaccess. We will do the rewriting from the main Apache config.

Here is the example Apache config. I have split it into two files in this example. We run on a Debian system, and Apache has a layout with a conf.d and a sites-available/sites-enabled. We are also using mod_macro for the configuration, as this allows us to reduce configuration duplication.

File: /etc/apache2/conf.d/mageno

<Macro MagentoVHost $store $langs>
  Options +FollowSymlinks
  DocumentRoot /path/to/site/store
  SetEnv MAGENTO_STORE $store
  # For information on the next variable, see earlier blog post.
  # SetEnv MAGENTO_DEVURLS /path/to/urls.ini
  RewriteEngine On
  # we always have English as our default language
  RedirectMatch ^/$ /en/
  RewriteRule ^/($langs)/(.*)$ /index.php/$2 [E=MAGENTO_LANG:$1,L]
</Macro>

<Directory /path/to/site/store>
  RewriteEngine On
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteCond %{REQUEST_FILENAME} !-l
  RewriteRule ^(.*)$ /index.php/$2 [L]
</Directory>

File: /etc/apache2/sites-available/store1.example.com

<VirtualHost *:80>
  ServerName store1.example.com
  # this store has 5 languages (store views)
  Use MagentoVHost store1 en|fr|es|de|nl
</VirtualHost>

<IfModule mod_ssl.c>
  <VirtualHost *:443>
    ServerName store1.example.com    # this store has 5 languages (store views)
    Use MagentoVHost store1 en|fr|es|de|nl

    SSLEngine on
    SSLCertificateFile /etc/apache2/ssl/your_certificate.pem

  </VirtualHost>
</IfModule>

Now a brief explanation on how the above works. All stores use a language in the url. If you visit the root,  the

RedirectMatch ^/$ /en/

line will redirect to the default store view (English). The virtualhost example above passes the store name to our macro. The macro then sets the variable MAGENTO_STORE to this. Depending on the language selected (from the chosen url) Apache will set an environment variable MAGENTO_LANG. So if we visited http://store1.example.com/de/ MAGENTO_STORE would be set to “store1” and MAGENTO_LANG would be set to “de”. Now we just need to process these in the main index.php that “Runs” the Magento store of our choice.

File: site/store/index.php
The following code replaced the last two lines in the default magento config

umask(0);
Mage::run();

# we use the url structure as /en/blah.html /fr/blah.html for each language
# language is set by mod_rewrite from the apache config
$lang = 'en';
if ( isset($_SERVER['MAGENTO_LANG']) ) {
  $lang = $_SERVER['MAGENTO_LANG'];
}

# store is set from the apache configuration depending on domain.
# this allows us to configure stores without having to modify multiple index.php files etc.
$store = '';
if ( isset($_SERVER['MAGENTO_STORE']) ) {
  $store = $_SERVER['MAGENTO_STORE'];
}
$store .= '_' . $lang;

# replace the REQUEST_URI such as /en/blah.html with /blah.html
$_SERVER['REQUEST_URI'] = preg_replace(&quot;#^/$lang(/.*)#i&quot;,'$1',$_SERVER['REQUEST_URI']);

# we want our /lang/ urls to take priority over store querystrings
# but not if we are in the admin panel
if ( ! preg_match('#^/index.php/admin/#i',$_SERVER['REQUEST_URI']) ) {
  $_GET['___store'] = $store;
}

umask(0);
Mage::run($store,'store');

The comments in the code should explain how it works. We construct our store name from the store name and language. In our example this would be store1_de. The language is then stripped from the REQUEST_URI environment variable, as this breaks magento’s search engine URL processing. (So even though we are at the URL http://store1.example.com/de/somepage magento thinks we are at http://store1.example.com/somepage. Also note the code to override the ___store querystring parameter. We want our url path to specify which store we are in over any querystring magento chooses. This solves an issue I had where Magento would think we are in the wrong store. Perhaps due to the “current” store being stored in a cookie or a querystring parameter being appended by Magento. This parameter is also used on the administration side, so we don’t want to interfere with if we are in the admin panel. Finally we “run” the store of our choice.

Hopefully this has made sense. It might look rather complicated, but does allow for a flexible setup. If we wanted to add a new store view (language) to our example store1, we can simply add it from the magento admin panel, and modify the apache config. The index.php does not need to change.

Magento Base URLs and dev/staging installations

One of the earlier problems I came across when setting up Magento, was that the Base URLs for the websites/stores are stored in the database. We wanted a scenario where we can have a staging site, and a development site, and transfer the database between them. With the way Magento works, this would require updating the Base URLs each time, and means that we couldn’t easily share a single database between a local development copy and a shared development copy running from our svn.

My first idea to workaround this was to create a small tool, that updated the Base URLs stored in the database, however this didn’t solve the issue of sharing one database between different installs. I looked into the Magento code, and saw that there is a function Mage::getBaseUrl (which in turn calls Mage_Core_Model_Store->getBaseUrl). If I could override this function on the development copies, I could replace the live Base URLs stored in the database with whatever local address we were using.

One of the best places to find development information seems to be on other people’s blogs, and this was one of the reasons why we thought it a good idea to blog our development here. I found http://www.belanur.de/blog/2008/09/05/overriding-core-models-with-a-custom-module/ invaluable when it came to learn how to override classes/function in Magento.

On to the code. The code is split into 4 files. Two XML configurations, and the php with the new getBaseUrl call. The other file is a ini config file for specifying how the Base URLs should map to the development/staging URLs.

Assuming you have a working configured Magento install. The following paths are all referenced relative to the main Magento folder.

File: app/etc/modules/Maglife_All.xml
This file activates our module. Magento merges this xml with its own xml to create a config.

<?xml version="1.0"?>
<config>
    <modules>
      <Maglife_Core>
        <active>true</active>
        <codePool>local</codePool>
      </Maglife_Core>
    </modules>
</config>

File: app/code/local/Maglife/Core/etc/config.xml
This file specifies to Magento what the module does. In this case it says the model / class we want to override and points to the replacement file (the line Maglife_Core_Model_Store translates to the path Maglife/Core/Model/Store.php which contains our code). This is at least my current understanding as I haven’t found any comprehensive documentation on the various xml tags yet.

<?xml version="1.0"?>
<config>
    <global>
        <models>
            <core>
                <rewrite>
                    <store>Maglife_Core_Model_Store</store>
                </rewrite>
            </core>
        </models>
    </global>
</config>

File: app/code/local/Maglife/Core/Model/Store.php
This contains the actual code. We call the parent getBaseUrl function, and then if the ENV variable is set, we replace the base url with a url provided in the urls.ini file (passed in the MAGENTO_DEVURLS variable).

<?php

class Maglife_Core_Model_Store extends Mage_Core_Model_Store
{

    public function getBaseUrl($type=self::URL_TYPE_LINK, $secure=null)
    {
        $store_code = $this->getCode();
        $url = parent::getBaseUrl($type, $secure);
        if ($url_ini = @$_SERVER['MAGENTO_DEVURLS'])
        {
            if ($urls = parse_ini_file($url_ini))
            {
                $host = parse_url($url, PHP_URL_HOST);
                if (isset($urls[$host]))
                {
                    $url = str_replace('://'.$host.'/', '://'.$urls[$host].'/', $url);
                }
            }
        }
        return $url;
    }

}

?>

File: urls.ini (can call this anything you like)

livestore.example.com = staging-livestore.example.com
livestore2.example.com = staging-livestore2.example.com

Then in my apache config on the staging/dev site I have the line

SetEnv MAGENTO_DEVURLS /path/to/urls.ini

and that’s it. I can now copy our live database to the staging server for example, without having to manually alter the Base URL config data in the database. I hope this is some use to others.

Magento Blog

We chose the Magento software for a new e-commerce project, and decided it would be a good idea to document our findings and ideas for others to use. Magento documentation is a little thin in places, and we have found a good deal of information from reading other people’s blogs.