🧩

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

PasoHook / MetodoTecnica
Mostrar datoshookActionProductFormBuilderModifierSQL directo + inyeccion en 'data' del create()
Guardar datoshookActionAfterUpdateProductFormHandlerLeer $_POST directamente, REPLACE INTO
DataProvider hookhookActionProductFormDataProviderDataDEJAR VACIO — no usarlo para tabs propios
Indexacion de idiomasDoble clave ISO + IDcontent['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

PuntoDescripcionEstado
Tabla DBid_product + id_shop + id_lang como clave compuestaRequerido
FormBuilderSQL directo + 'data' => array en create()Requerido
Doble indexacioncontent['es'] Y content[1] simultaneamenteRequerido
DataProvider hookDejar VACIO para tabs propiosRequerido
Save hookLeer $_POST['product']['tab_name'] directamenteRequerido
REPLACE INTOOperacion atomica insert-or-updateRequerido
Front JSprestashop.on('updatedProduct', ...) para re-inicializarSi usas JS
Descargar en Markdown Pensado para pegar en ChatGPT, Claude u otra IA. Incluye solo el contenido de esta pagina.