Nginx Virtual Store Subdirectories

Updated . Posted . Visible to the public.

Note - this article only applies to nginx and PHP-FPM!

If you want to have stores which are accessed via subdirectories as opposed to domain names, the standard method I've found is to make a directory with a copy of index.php in it, using one of the following approaches:

  • Leaving index.php exactly as-is, and calling fastcgi_param MAGE_RUN_CODE us; in your nginx config
  • Changing index.php to call a hardcoded string of us, either by altering the call to Mage::run(), or by setting $mageRunCode

I'm not happy with either of the approaches. It's pedantic, no question about it, but it really irritates me to a single entry point, and a method of dynamically setting the store being accessed (MAGE_RUN_CODE) and not using it. The solution is below - obviously you need to change the two occurrences of us to your store code, and /var/www to your Magento document root.

location ~* ^/us(?<store_uri>.*) {
  root /var/www;
  include /etc/nginx/fastcgi_params;
  fastcgi_param REQUEST_URI $store_uri?$args;
  fastcgi_param SCRIPT_FILENAME $document_root/index.php;
  fastcgi_param SCRIPT_NAME /index.php;
  fastcgi_param DOCUMENT_URI /index.php;
  fastcgi_param MAGE_RUN_CODE us;
  fastcgi_param MAGE_RUN_TYPE store;
  fastcgi_pass 127.0.0.1:9000;
}

With the above code, if not accessed via /us/* then our default store will be served, but if access via /us/* then the us store will be served, and /us/ will be stripped from the URI so that Magento doesn't try to serve a page like us/some/product.html from the us store.

The Attempt That Went Wrong

I tried initially using rewrites and failed dow to how Magento finds out its base URL (well, actually, it's Zend) - here is what happens:

  • A customer would try to access /us/some/product.html
  • nginx would rewrite everything in us to be non-us
  • PHP-FPM would run index.php, but
  • nginx would have given PHP-FPM the document root of /var/www and a request URI of /us/some/product.html

Now what happens is that Magento would remove /var/www from /us/some/product.html - being as nothing was in common, Magento now thought that the customer wanted /us/some/product.html from the us store, which obviously doesn't exist, but it would if we stripped /us off the front, so that's what we do in the solution above.

If you're interested, the logic that causes this behaviour is Zend_Controller_Request_Http, which is called through the following stack:

  • Mage_Core_Model_App calls initRequest()
    • That calls setPathInfo() on Mage_Core_Controller_Request_Http
      • That calls getBaseUrl() on Zend_Controller_Request_Http
        • That calls setBaseUrl() on Zend_Controller_Request_Http

The method responsible is below:

public function setBaseUrl($baseUrl = null)
{
    if ((null !== $baseUrl) && !is_string($baseUrl)) {
        return $this;
    }

    if ($baseUrl === null) {
        $filename = (isset($_SERVER['SCRIPT_FILENAME'])) ? basename($_SERVER['SCRIPT_FILENAME']) : '';

        if (isset($_SERVER['SCRIPT_NAME']) && basename($_SERVER['SCRIPT_NAME']) === $filename) {
            $baseUrl = $_SERVER['SCRIPT_NAME'];
        } elseif (isset($_SERVER['PHP_SELF']) && basename($_SERVER['PHP_SELF']) === $filename) {
            $baseUrl = $_SERVER['PHP_SELF'];
        } elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $filename) {
            $baseUrl = $_SERVER['ORIG_SCRIPT_NAME']; // 1and1 shared hosting compatibility
        } else {
            // Backtrack up the script_filename to find the portion matching
            // php_self
            $path    = isset($_SERVER['PHP_SELF']) ? $_SERVER['PHP_SELF'] : '';
            $file    = isset($_SERVER['SCRIPT_FILENAME']) ? $_SERVER['SCRIPT_FILENAME'] : '';
            $segs    = explode('/', trim($file, '/'));
            $segs    = array_reverse($segs);
            $index   = 0;
            $last    = count($segs);
            $baseUrl = '';
            do {
                $seg     = $segs[$index];
                $baseUrl = '/' . $seg . $baseUrl;
                ++$index;
            } while (($last > $index) && (false !== ($pos = strpos($path, $baseUrl))) && (0 != $pos));
        }

        // Does the baseUrl have anything in common with the request_uri?
        $requestUri = $this->getRequestUri();

        if (0 === strpos($requestUri, $baseUrl)) {
            // full $baseUrl matches
            $this->_baseUrl = $baseUrl;
            return $this;
        }

        if (0 === strpos($requestUri, dirname($baseUrl))) {
            // directory portion of $baseUrl matches
            $this->_baseUrl = rtrim(dirname($baseUrl), '/');
            return $this;
        }

        $truncatedRequestUri = $requestUri;
        if (($pos = strpos($requestUri, '?')) !== false) {
            $truncatedRequestUri = substr($requestUri, 0, $pos);
        }

        $basename = basename($baseUrl);
        if (empty($basename) || !strpos($truncatedRequestUri, $basename)) {
            // no match whatsoever; set it blank
            $this->_baseUrl = '';
            return $this;
        }

        // If using mod_rewrite or ISAPI_Rewrite strip the script filename
        // out of baseUrl. $pos !== 0 makes sure it is not matching a value
        // from PATH_INFO or QUERY_STRING
        if ((strlen($requestUri) >= strlen($baseUrl))
            && ((false !== ($pos = strpos($requestUri, $baseUrl))) && ($pos !== 0)))
        {
            $baseUrl = substr($requestUri, 0, $pos + strlen($baseUrl));
        }
    }

    $this->_baseUrl = rtrim($baseUrl, '/');
    return $this;
}

The important line is this bit:

$filename = (isset($_SERVER['SCRIPT_FILENAME'])) ? basename($_SERVER['SCRIPT_FILENAME']) : '';

if (isset($_SERVER['SCRIPT_NAME']) && basename($_SERVER['SCRIPT_NAME']) === $filename) {
    $baseUrl = $_SERVER['SCRIPT_NAME'];
Mike Whitby
Last edit
Posted by Mike Whitby to Magento (2014-05-19 16:49)