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 callingfastcgi_param MAGE_RUN_CODE us;
in your nginx config - Changing
index.php
to call a hardcoded string ofus
, either by altering the call toMage::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
callsinitRequest()
- That calls
setPathInfo()
onMage_Core_Controller_Request_Http
- That calls
getBaseUrl()
onZend_Controller_Request_Http
- That calls
setBaseUrl()
onZend_Controller_Request_Http
- That calls
- That calls
- That calls
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'];