We won’t focus on back-office opportunities since you can get acquainted with solid documentation here. Let’s study technical issues of this opportunity instead.
The multistore feature allows you to manage several stores with one back-office. To start using a multistore, activate it in Preferences -> General -> Enable Multistore. If this feature is enabled, a Multistore tab will be available in the Advanced Parameters menu.
Now we can manage multistores via Advanced Parameters -> Multistore. Click the Add new shop link to create several stores.
Upon store creation, define its future URL by moving to Add URL.
This parameter is specified in the Virtual URI field. Our new store will be available under address, noted in the Your final URL will be field.
URL virtual data is stored in the database in the shop_url table.
When a user jumps to some page, index.php file always appears as initial entry point unlike in a former PrestaShop version. Back then each controller was initiated in a corresponding file, for example, product.php.:
Prestashop 1.4.x ->
require(dirname(__FILE__).’/config/config.inc.php’); // initialization file activation
ControllerFactory::getController(‘ProductController’)->run(); //controller activation
PrestaShop 1.5.x ->
require(dirname(__FILE__).’/config/config.inc.php’); // initialization file activation
Tools::displayFileAsDeprecated(); // displaying notification that the file used is outdated
Tools::redirect(‘index.php?controller=product’.($_REQUEST ? ‘&’.http_build_query($_REQUEST, ”, ‘&’) : ”), __PS_BASE_URI__, null, ‘HTTP/1.1 301 Moved Permanently’); // if a constant (_PS_DISPLAY_COMPATIBILITY_WARNING_) is set to FALSE, redirect to index.php with a specified controller will be performed instead of displaying an error.
Below I introduce the function that notifies about outdated controller request:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public static function displayFileAsDeprecated() { $backtrace = debug_backtrace(); $callee = current($backtrace); $error = 'File <b>'.$callee['file'].'</b> is deprecated<br />'; $message = 'The file '.$callee['file'].' is deprecated and will be removed in the next major version.'; $class = isset($callee['class']) ? $callee['class'] : null; Tools::throwDeprecated($error, $message, $class); } protected static function throwDeprecated($error, $message, $class) { if (_PS_DISPLAY_COMPATIBILITY_WARNING_) { // trigger_error($error, E_USER_WARNING); Logger::addLog($message, 3, $class); } } |
Note that the id_store parameter takes part in many SQL-requests in a new PrestaShop version. So how does the system understand which store data should be used? It happens in the config/config.inc.php file:
1 2 |
/* Initialize the current Shop */ Context::getContext()->shop = Shop::initialize(); |
getContext() method returns us a singleton pattern. This method shows up in almost all PrestaShop controllers and classes.
1 2 3 4 5 6 7 8 9 10 11 |
/** * Get a singleton context * * @return Context */ public static function getContext() { if (!isset(self::$instance)) self::$instance = new Context(); return self::$instance; } |
Let’s examine the Shop::initialize(); method closely.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
** * Find the shop from current domain / uri and get an instance of this shop * if INSTALL_VERSION is defined, will return an empty shop object * * @return Shop */ public static function initialize() { // Find current shop from URL if (!($id_shop = Tools::getValue('id_shop')) || defined('_PS_ADMIN_DIR_')) { // if id_shop was not transferred openly, this parameter will be calculated based on database data $host = pSQL(Tools::getHttpHost()); $sql = 'SELECT s.id_shop, CONCAT(su.physical_uri, su.virtual_uri) AS uri, su.domain, su.main FROM '._DB_PREFIX_.'shop_url su LEFT JOIN '._DB_PREFIX_.'shop s ON (s.id_shop = su.id_shop) WHERE (su.domain = \''.$host.'\' OR su.domain_ssl = \''.$host.'\') AND s.active = 1 AND s.deleted = 0 ORDER BY LENGTH(uri) DESC'; $id_shop = ''; $found_uri = ''; $request_uri = rawurldecode($_SERVER['REQUEST_URI']); $is_main_uri = false; if ($results = Db::getInstance()->executeS($sql)) { foreach ($results as $row) { // An URL matching current shop was found if (preg_match('#^'.preg_quote($row['uri'], '#').'#', $request_uri)) { $id_shop = $row['id_shop']; $found_uri = $row['uri']; if ($row['main']) $is_main_uri = true; break; } } } // If an URL was found but is not the main URL, redirect to main URL if ($id_shop && !$is_main_uri) { foreach ($results as $row) { if ($row['id_shop'] == $id_shop && $row['main']) { // extract url parameters $request_uri = substr($request_uri, strlen($found_uri)); header('HTTP/1.1 301 Moved Permanently'); header('Cache-Control: no-cache'); header('location: http://'.$row['domain'].$row['uri'].$request_uri); exit; } } } } if (!$id_shop && defined('_PS_ADMIN_DIR_')) { // If in admin, we can access to the shop without right URL $shop = new Shop(Configuration::get('PS_SHOP_DEFAULT')); $shop->physical_uri = preg_replace('#/+#', '/', str_replace('\\', '/', dirname(dirname($_SERVER['SCRIPT_NAME']))).'/'); $shop->virtual_uri = ''; } else { $shop = new Shop($id_shop); if (!Validate::isLoadedObject($shop) || !$shop->active || !$id_shop) { // No shop found ... too bad, let's redirect to default shop $default_shop = new Shop(Configuration::get('PS_SHOP_DEFAULT')); // Hmm there is something really bad in your Prestashop ! if (!Validate::isLoadedObject($default_shop)) throw new PrestaShopException('Shop not found'); $params = $_GET; unset($params['id_shop']); if (!Configuration::get('PS_REWRITING_SETTINGS')) $url = 'http://'.$default_shop->domain.$default_shop->getBaseURI().'index.php?'.http_build_query($params); else { // Catch url with subdomain "www" if (strpos($default_shop->domain, 'www.') === 0 && 'www.'.$_SERVER['HTTP_HOST'] === $default_shop->domain || $_SERVER['HTTP_HOST'] === 'www.'.$default_shop->domain) $uri = $default_shop->domain.$_SERVER['REQUEST_URI']; else $uri = $default_shop->domain.$default_shop->getBaseURI(); if (count($params)) $url = 'http://'.$uri.'?'.http_build_query($params); else $url = 'http://'.$uri; } header('location: '.$url); exit; } } self::$context_id_shop = $shop->id; self::$context_id_shop_group = $shop->id_shop_group; self::$context = self::CONTEXT_SHOP; return $shop; } |
System defines in which store we are located based on id_store transfer or database info (on the grounds of function results Tools::getHttpHost (return the <b>current</b> host used)).
As you probably guessed, there are some changes in a database structure. Now one more table is added to every entity, except its own table where all data is stored. This new table includes differences for multiple stores and looks like a table with an entity name + store (example: attribute_group_shop, attribute_shop, carrier_shop, etc.). Let’s review it, using manufacturer as an example.
Configuration for the entire group:
BelVG manufacturer for Prestashop 1.5 store is missing.
BelVG manufacturer for Test store is available.
All manufacturers are presented in the manufacturer table.
Manufacturer_shop table points out connections between manufacturers and stores.
This was a quite simple case because store data is the same.
In the case of products, there is more data that will be different from store to store. That is why the product_shop table contains more columns.
Please, keep in mind that product_shop stores data may vary from store to store. Let’s change the price of a MacBook in one of the stores. The expected result – product entry will remain unchanged in the product table while in the product_shop table we will see a new value.
Images above display different prices in different stores.
The following changes were detected in the database:
1) in the product_shop tables prices for different stores were changed
2) in the product table the result correlates with the last updated price
That’s all for now. I hope this post will help you comprehend a new model, used by PrestaShop. Would you like to make any corrections or do you disagree with me at some point? Feel free to tell me your opinion in the comment section below.
Pedro and benjamin, please contact us through our contact form.
Zeljko, in the standard version you cant do this without Prestashop customization.
Is it possible to make in the multistor following situation, we have a couple of stores, each with its own products and own subdomain and they have the same categories, but on the main domain it will show all the products from all stores from category that somebody select?
Thank you in advance for every help
Hi Alex,
I’m wondering if you could contract for us in setting up our multistore?
All i read about multi store seems great, but not quite qhat i need. And seems much more simpler.
We have a domain .com .pt and .es
In prestashop 1.4 we had the module domainbylang which sorted the languages for each domain really well.
If we clicked on another language, it would also change the domain.
In multi store, it seems we need to create a store for each language, which makes no sense, since our prices are the same, template, stock, customers,…
I’ve added the other 2 domain to the same shop, but the links for products, categories,… have the main url. And if i deactivate this i get a load of errors.
In conclusion, prestashop developers should have thought about this situation. Specially since someone had a module for that usually means someone uses that feature…
Pedro