🏪 Modulo compatible con multitienda — patron completo
Actualizado: 2025-01-15
#Que implica multitienda para un modulo
Cuando multitienda esta activa, el admin puede estar en 3 contextos: 'Todas las tiendas', un grupo de tiendas, o una tienda individual. Tu modulo debe respetar este contexto en: lectura/escritura de Configuration, queries a la BD, y formularios del BO.
Error comun
El error #1 en multitienda es usar Configuration::get() y Configuration::updateValue() sin entender que leen/escriben para la tienda actual del contexto. Si el admin esta en 'Todas las tiendas' y tu modulo guarda una config, se guarda para TODAS. Si esta en Tienda 2, se guarda solo para Tienda 2.
#Configuration por tienda
Leer y escribir config respetando el contexto
php
<?php
/**
* Configuration::get() lee automaticamente para la tienda del contexto actual.
* Pero a veces necesitas leer para una tienda especifica:
*/
// Lee para la tienda actual del contexto (lo normal)
$value = Configuration::get('MI_CONFIG');
// Lee para una tienda especifica (forzar id_shop)
$valueShop2 = Configuration::get('MI_CONFIG', null, null, 2); // id_shop = 2
// Lee el valor global (no vinculado a ninguna tienda)
$globalValue = Configuration::getGlobalValue('MI_CONFIG');
// Escribir para la tienda actual del contexto
Configuration::updateValue('MI_CONFIG', 'valor');
// Escribir para una tienda especifica
Configuration::updateValue('MI_CONFIG', 'valor_tienda2', false, 0, 2);
// Escribir valor global (todas las tiendas)
Configuration::updateGlobalValue('MI_CONFIG', 'valor_global');
/**
* Patron recomendado: defaults en install()
* Guardar valores por defecto para CADA tienda
*/
public function install()
{
if (!parent::install()) {
return false;
}
// Iterar todas las tiendas para poner valores por defecto
$shops = Shop::getShops(true, null, true); // Solo IDs
foreach ($shops as $idShop) {
Configuration::updateValue('ECOM_BANNER_ENABLED', 1, false, 0, $idShop);
Configuration::updateValue('ECOM_BANNER_TEXT', 'Envio gratis +50 EUR', false, 0, $idShop);
Configuration::updateValue('ECOM_BANNER_COLOR', '#2fb5d2', false, 0, $idShop);
}
return true;
}
#ObjectModel con shop context
ObjectModel con tabla _shop para multitienda
php
<?php
/**
* Para que un ObjectModel sea multitienda, necesitas:
* 1. Una tabla principal: ps_ecom_banner
* 2. Una tabla _shop: ps_ecom_banner_shop
* 3. 'multishop' => true en $definition
*/
class EcomBanner extends ObjectModel
{
public $id_ecom_banner;
public $title;
public $content;
public $active;
public $position;
public $date_add;
public static $definition = [
'table' => 'ecom_banner',
'primary' => 'id_ecom_banner',
'multilang' => true,
'multishop' => true, // <-- Clave para multitienda
'fields' => [
'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool', 'shop' => true],
'position' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'shop' => true],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
// Campos multilang
'title' => ['type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'lang' => true, 'required' => true, 'size' => 255],
'content' => ['type' => self::TYPE_HTML, 'validate' => 'isCleanHtml', 'lang' => true, 'size' => 65535],
],
];
/**
* Obtener banners activos para la tienda actual
*/
public static function getActiveBanners(int $idLang, int $idShop): array
{
return Db::getInstance()->executeS(
(new DbQuery())
->select('b.*, bl.title, bl.content')
->from('ecom_banner', 'b')
->innerJoin('ecom_banner_shop', 'bs',
'b.id_ecom_banner = bs.id_ecom_banner AND bs.id_shop = ' . $idShop)
->innerJoin('ecom_banner_lang', 'bl',
'b.id_ecom_banner = bl.id_ecom_banner AND bl.id_lang = ' . $idLang)
->where('bs.active = 1') // active viene de tabla _shop
->orderBy('bs.position ASC') // position viene de tabla _shop
->build()
) ?: [];
}
}
SQL: tablas con soporte multitienda
sql
-- Tabla principal (datos compartidos)
CREATE TABLE IF NOT EXISTS `PREFIX_ecom_banner` (
`id_ecom_banner` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`date_add` DATETIME NOT NULL,
PRIMARY KEY (`id_ecom_banner`)
) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8mb4;
-- Tabla _shop (datos por tienda: active, position, etc.)
CREATE TABLE IF NOT EXISTS `PREFIX_ecom_banner_shop` (
`id_ecom_banner` INT(10) UNSIGNED NOT NULL,
`id_shop` INT(10) UNSIGNED NOT NULL,
`active` TINYINT(1) NOT NULL DEFAULT 1,
`position` INT(10) UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id_ecom_banner`, `id_shop`)
) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8mb4;
-- Tabla _lang (datos por idioma)
CREATE TABLE IF NOT EXISTS `PREFIX_ecom_banner_lang` (
`id_ecom_banner` INT(10) UNSIGNED NOT NULL,
`id_lang` INT(10) UNSIGNED NOT NULL,
`title` VARCHAR(255) NOT NULL,
`content` TEXT,
PRIMARY KEY (`id_ecom_banner`, `id_lang`)
) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8mb4;
#Queries con filtro de tienda
Siempre filtrar por id_shop en queries
php
<?php
// INCORRECTO — devuelve datos de TODAS las tiendas
$products = Db::getInstance()->executeS(
'SELECT * FROM `' . _DB_PREFIX_ . 'product` WHERE active = 1'
);
// CORRECTO — filtra por tienda actual
$idShop = (int) Context::getContext()->shop->id;
$products = Db::getInstance()->executeS(
(new DbQuery())
->select('p.*, ps.active, ps.price')
->from('product', 'p')
->innerJoin('product_shop', 'ps',
'p.id_product = ps.id_product AND ps.id_shop = ' . $idShop)
->where('ps.active = 1')
->build()
);
// Contexto multitienda: respetar el contexto del admin (BO)
if (Shop::isFeatureActive()) {
$shopContext = Shop::getContext();
switch ($shopContext) {
case Shop::CONTEXT_ALL:
// Sin filtro de tienda — mostrar todo
break;
case Shop::CONTEXT_GROUP:
$idGroup = (int) Shop::getContextShopGroupID();
$shopIds = Shop::getShops(true, $idGroup, true);
$sql->where('ps.id_shop IN (' . implode(',', array_map('intval', $shopIds)) . ')');
break;
case Shop::CONTEXT_SHOP:
$sql->where('ps.id_shop = ' . (int) Shop::getContextShopID());
break;
}
}
#Formulario que respeta el contexto
HelperForm con indicador de tienda
php
<?php
public function getContent()
{
$output = '';
// Mostrar aviso de contexto multitienda
if (Shop::isFeatureActive()) {
$shopContext = Shop::getContext();
if ($shopContext === Shop::CONTEXT_ALL) {
$output .= $this->displayWarning(
'Estas editando la configuracion para TODAS las tiendas. '
. 'Selecciona una tienda especifica para configurar individualmente.'
);
}
}
if (Tools::isSubmit('submitConfig')) {
// Configuration::updateValue() respeta el contexto automaticamente
Configuration::updateValue('ECOM_BANNER_ENABLED', (int) Tools::getValue('ECOM_BANNER_ENABLED'));
Configuration::updateValue('ECOM_BANNER_TEXT', Tools::getValue('ECOM_BANNER_TEXT'));
$output .= $this->displayConfirmation('Guardado correctamente.');
}
return $output . $this->renderForm();
}
#Checklist de compatibilidad
- Configuration::get/updateValue sin id_shop explicito (respeta contexto automaticamente)
- Queries a product/category/customer siempre con JOIN a tabla _shop
- ObjectModel con 'multishop' => true si los datos varian por tienda
- install() registra valores por defecto para cada tienda existente
- Hooks de front leen Context::getContext()->shop->id para filtrar datos
- AdminController usa Shop::getContextShopID() en las queries del listado
- No hardcodeas id_shop = 1 en ninguna parte
- Testeas con al menos 2 tiendas activas en contextos: all, group, shop
Descargar en Markdown
Pensado para pegar en ChatGPT, Claude u otra IA. Incluye solo el contenido de esta pagina.