🧩
Tabs extra en el formulario de producto (BO) — Patron Direct Injection
Actualizado: 2024-12-01
Añadir tabs multilingue con TinyMCE al formulario de producto de PS 8 es complejo por el sistema de Data Providers de Symfony. El patron Direct Injection evita los problemas mas comunes (campos vacios al guardar, datos que no se muestran) inyectando los datos directamente en el FormBuilder y leyendo $_POST directamente al guardar.
Por que NO usar DataProvider hook para tabs propios
El hook actionProductFormDataProviderData es ideal para modificar campos EXISTENTES del core. Para campos propios en tabs nuevos, usar Direct Injection en hookActionProductFormBuilderModifier es mas fiable y evita problemas de indexacion de idiomas.
#El patron Direct Injection
| Paso | Hook / Metodo | Tecnica |
|---|---|---|
| Mostrar datos | hookActionProductFormBuilderModifier | SQL directo + inyeccion en 'data' del create() |
| Guardar datos | hookActionAfterUpdateProductFormHandler | Leer $_POST directamente, REPLACE INTO |
| DataProvider hook | hookActionProductFormDataProviderData | DEJAR VACIO — no usarlo para tabs propios |
| Indexacion de idiomas | Doble clave ISO + ID | content['es'] Y content[1] simultaneamente |
#Estructura de base de datos
Tabla con clave compuesta id_product + id_shop + id_lang
sql
CREATE TABLE IF NOT EXISTS `{prefix}mymodule_product_content` (
`id_product` int(11) UNSIGNED NOT NULL,
`id_shop` int(11) UNSIGNED NOT NULL,
`id_lang` int(11) UNSIGNED NOT NULL,
`extra_content` TEXT,
`extra_title` VARCHAR(255) DEFAULT '',
PRIMARY KEY (`id_product`, `id_shop`, `id_lang`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#Form Builder (carga de datos)
hookActionProductFormBuilderModifier — Direct Injection con doble indexacion
php
<?php
use Symfony\Component\Form\Extension\Core\Type\FormType;
use PrestaShopBundle\Form\Admin\Type\TranslateType;
use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;
public function install(): bool
{
return parent::install()
&& $this->registerHook('actionProductFormBuilderModifier')
&& $this->registerHook('actionAfterUpdateProductFormHandler')
&& $this->registerHook('actionAfterCreateProductFormHandler');
}
public function hookActionProductFormBuilderModifier(array $params): void
{
$formBuilder = $params['form_builder'];
$idProduct = (int) ($params['id'] ?? 0);
$idShop = (int) Context::getContext()->shop->id;
$languages = Language::getLanguages(true);
// PASO 1: Fetch SQL directo
$rows = [];
if ($idProduct > 0) {
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . 'mymodule_product_content`
WHERE `id_product` = ' . $idProduct . '
AND `id_shop` = ' . $idShop;
$rows = Db::getInstance()->executeS($sql) ?: [];
}
// Indexar resultados por id_lang para lookup rapido
$byLang = [];
foreach ($rows as $row) {
$byLang[(int) $row['id_lang']] = $row;
}
// PASO 2: Preparar array con DOBLE indexacion (ISO + ID)
$content = [];
$title = [];
foreach ($languages as $lang) {
$idLang = (int) $lang['id_lang'];
$iso = $lang['iso_code'];
$val = $byLang[$idLang]['extra_content'] ?? '';
$tit = $byLang[$idLang]['extra_title'] ?? '';
$content[$iso] = $val; // Por ISO (es, en, fr...)
$content[$idLang] = $val; // Por ID (1, 2, 3...)
$title[$iso] = $tit;
$title[$idLang] = $tit;
}
// PASO 3: Crear el tab e inyectar datos directamente
$tabBuilder = $formBuilder->create('mymodule_extra_tab', FormType::class, [
'label' => $this->trans('Contenido Extra', [], 'Modules.Mymodule.Admin'),
'inherit_data' => false,
'mapped' => false,
'required' => false,
'data' => ['content' => $content, 'title' => $title], // <-- CLAVE
]);
$tabBuilder->add('content', TranslateType::class, [
'type' => FormattedTextareaType::class, // TinyMCE
'label' => $this->trans('Contenido HTML', [], 'Modules.Mymodule.Admin'),
'locales' => $languages,
'required' => false,
]);
$formBuilder->add($tabBuilder);
}
#Hook de guardado (Raw POST)
hookActionAfterUpdateProductFormHandler — lectura directa de $_POST
php
<?php
public function hookActionAfterUpdateProductFormHandler(array $params): void
{
$this->saveProductContent($params);
}
public function hookActionAfterCreateProductFormHandler(array $params): void
{
$this->saveProductContent($params);
}
private function saveProductContent(array $params): void
{
// LECTURA DIRECTA DE $_POST (no usar $params['form_data'] para tabs custom)
// Estructura: product[mymodule_extra_tab][content][1] = 'contenido en id_lang 1'
$moduleData = $_POST['product']['mymodule_extra_tab'] ?? null;
if (!$moduleData) {
return;
}
$idProduct = (int) $params['id'];
$idShop = (int) Context::getContext()->shop->id;
$languages = Language::getLanguages(true);
foreach ($languages as $lang) {
$idLang = (int) $lang['id_lang'];
$iso = $lang['iso_code'];
// Prioridad: buscar por ID, fallback a ISO
$content = $moduleData['content'][$idLang]
?? $moduleData['content'][$iso]
?? '';
// REPLACE INTO para insert o update atomico
Db::getInstance()->execute(
'REPLACE INTO `' . _DB_PREFIX_ . 'mymodule_product_content`
(`id_product`, `id_shop`, `id_lang`, `extra_content`)
VALUES (' . $idProduct . ', ' . $idShop . ', ' . $idLang . ',
"' . pSQL($content, true) . '")'
);
}
}
#Re-inicializar en el Front Office
Obligatorio: re-inicializar JS al cambiar combinacion en el Front
javascript
// SIEMPRE incluir esto si tu modulo tiene logica JS en el front
// El evento 'updatedProduct' se dispara al cambiar combinacion (color, talla)
(function() {
function initMyModule() {
// Re-enlazar eventos, inicializar componentes, etc.
}
document.addEventListener('DOMContentLoaded', initMyModule);
// OBLIGATORIO: sin esto el JS muere al cambiar combinacion
if (typeof prestashop !== 'undefined') {
prestashop.on('updatedProduct', function(event) {
initMyModule();
});
}
}());
#Checklist de implementacion
| Punto | Descripcion | Estado |
|---|---|---|
| Tabla DB | id_product + id_shop + id_lang como clave compuesta | Requerido |
| FormBuilder | SQL directo + 'data' => array en create() | Requerido |
| Doble indexacion | content['es'] Y content[1] simultaneamente | Requerido |
| DataProvider hook | Dejar VACIO para tabs propios | Requerido |
| Save hook | Leer $_POST['product']['tab_name'] directamente | Requerido |
| REPLACE INTO | Operacion atomica insert-or-update | Requerido |
| Front JS | prestashop.on('updatedProduct', ...) para re-inicializar | Si usas JS |
Descargar en Markdown
Pensado para pegar en ChatGPT, Claude u otra IA. Incluye solo el contenido de esta pagina.