Guía

Dónde engancharse en PrestaShop y cuándo conviene un override

Referencia para desarrolladores sobre hooks y overrides en PrestaShop: hooks de visualización, de acción, personalizados, sistema de overrides y migración.

Qué hacen realmente los hooks (y dónde se atasca la mayoría de la gente)

Cada módulo que publicamos en mypresta.rocks se conecta a PrestaShop mediante hooks, y, tras ejecutar 159 de ellos en producción, te podemos decir que casi todos los fallos de hooks que hemos corregido desde 2013 se reducen al mismo puñado de errores: registrado en el hook equivocado, no registrado en ninguna parte, disparado dos veces, o disparado donde el programador daba por hecho un override.

Es también la página que enviamos a los clientes cuando nos preguntan «¿por qué este módulo necesita una actualización para PrestaShop 9?», porque la respuesta es casi siempre «el hook que usaba ha cambiado».

Dos tipos de hooks, y la única regla que importa

PrestaShop tiene hooks de visualización (le piden a tu módulo HTML para renderizar) y hooks de acción (le comunican a tu módulo que ha ocurrido algo (pedido realizado, carrito guardado, cliente registrado) e ignoran lo que tú devuelvas).

La regla que pilla a la gente: el valor devuelto importa en los hooks de visualización, pero no en los de acción. Si tú haces return $this->display(...) desde un hook de acción, no pasa nada. Si olvidas el return en un hook de visualización, tu módulo no renderiza nada y perderás una hora preguntándote por qué.

Cómo llama PrestaShop realmente a su código

Cuando el núcleo alcanza un punto de hook, Hook::exec() consulta ps_hook_module, ordena los módulos activos por posición, llama al método hookXxx() de cada módulo con un array $params y (para los hooks de visualización) concatena el HTML devuelto.

Eso es todo. Sin magia. La razón por la que tu hook no se dispara es casi siempre una fila ausente en ps_hook_module, y no algo que esté mal en tu método.

Los cuatro patrones que usa la mayoría de los módulos

Antes de acudir a la documentación, pregúntate cuál de estas cuatro tareas estás realizando. Elige el patrón equivocado y lucharás contra el framework durante el resto de la vida del módulo.

Necesitas… Recurre a… No te molestes con…
Renderizar HTML en un tema o en una página del back officeUn hook display* que devuelva una plantilla SmartyHacer echo dentro de displayHeader
Reaccionar a un cambio de estado (pedido realizado, carrito guardado, producto actualizado)Un hook action*Un override del controlador
Añadir CSS o JS al front officeactionFrontControllerSetMedia + registerStylesheet / registerJavascriptEtiquetas <link> en bruto mediante displayHeader, anula el CCC y rompe el versionado de recursos
Cambiar un cálculo del núcleo que no tiene hookUn decorador de servicio Symfony (PS 8+) o, como último recurso, un overrideEditar archivos del núcleo

Los hooks que usará de verdad

PrestaShop incluye cientos de hooks. En todo nuestro catálogo de módulos nos registramos aproximadamente en una docena de ellos. Estos son los que vale la pena memorizar.

Front office: visualización

Hook Se dispara Úsalo para
displayProductAdditionalInfoDebajo del botón Añadir al carritoEstimaciones de entrega, guías de tallas, contenido extra en la página de producto: el espacio más valioso de una ficha de producto
displayShoppingCartPágina de resumen del carritoVentas cruzadas, calculadoras de envío, mensajes de urgencia
displayOrderConfirmationPágina de agradecimiento tras la compraSeguimiento de conversiones, ventas adicionales tras la compra, invitaciones a recomendar
displayCustomerAccountPágina «Mi cuenta»Secciones personalizadas, listas de deseos, puntos de fidelidad, solicitudes de RMA
displayBannerParte superior de la página, por encima de la cabeceraFranjas de rebajas, barras de envío gratuito, avisos de RGPD
displayHomeÁrea de contenido de la página de inicioColecciones destacadas, módulos hero, bloques HTML reutilizables

Back office: visualización

Hook Se dispara Úsalo para
displayAdminOrderPágina de detalle del pedidoPaneles personalizados, integraciones de transporte, estado de sincronización con el ERP, señales de fraude
displayAdminProductsExtraNueva pestaña en el editor de productosCampos de producto personalizados, datos de terceros
displayBackOfficeHeaderDentro del <head> del back officeCSS/JS del back office, widgets del panel, avisos de actualización

Hooks de acción

Hook Se dispara Úsalo para
actionValidateOrderPedido validado correctamenteConfirmación de pago, descuento de stock, flujo posterior al pago, el hook de acción más importante
actionOrderStatusUpdateCambia el estado del pedidoNotificaciones al transportista, sincronización con el ERP, correos al cliente
actionCartSaveCarrito creado o actualizadoDisparadores de carrito abandonado, reserva de inventario, analítica
actionProductUpdateProducto guardado en el back officeSincronización de catálogo externo, reindexación de búsqueda, regeneración de imágenes
actionCustomerAccountAddSe registra un cliente nuevoCorreo de bienvenida, sincronización con el CRM, segmentación
actionFrontControllerSetMediaEl controlador del front carga los recursosRegistrar CSS/JS de la forma correcta

El hook de recursos: por favor, use este, no displayHeader

Desde PrestaShop 1.7, el único lugar correcto para añadir CSS o JS al front office es actionFrontControllerSetMedia:

public function hookActionFrontControllerSetMedia($params)
{
    $this->context->controller->registerStylesheet(
        'my-module-style',
        'modules/' . $this->name . '/views/css/front.css',
        ['media' => 'all', 'priority' => 150]
    );

    $this->context->controller->registerJavascript(
        'my-module-script',
        'modules/' . $this->name . '/views/js/front.js',
        ['position' => 'bottom', 'priority' => 150]
    );
}

Los módulos que todavía inyectan etiquetas <link> y <script> en bruto a través de displayHeader rompen Combine-Compress-Cache (CCC), eluden la cadena de consulta para invalidar caché y no se pueden diferir. Cada vez que auditamos una tienda lenta, al menos dos módulos están haciendo esto. No seas uno de ellos.

Conectar un hook en su módulo

Registrar en la instalación

public function install()
{
    return parent::install()
        &amp;&amp; $this->registerHook('displayProductAdditionalInfo')
        &amp;&amp; $this->registerHook('actionCartSave')
        &amp;&amp; $this->registerHook('actionFrontControllerSetMedia');
}

Cada llamada a registerHook() inserta una fila en ps_hook_module. Si olvidas registrar, tu método de hook podría no existir, PrestaShop nunca lo llamará. Esta es la causa más habitual de «mi módulo no funciona» que vemos en soporte.

Otra trampa más: si añades un hook nuevo en la versión 1.2 de tu módulo después de haber publicado la 1.0, las instalaciones de los clientes existentes no reciben el registro. Necesitas un script de actualización (upgrade/install-1.2.0.php) que llame a registerHook(), o bien instrucciones para reiniciar el módulo. Incluye eso en las notas de la versión o recibirás tickets.

Implementar el método

El nombre del método = literalmente hook + el nombre del hook, con la primera letra en mayúscula:

public function hookDisplayProductAdditionalInfo($params)
{
    $product = $params['product'];

    $this->context->smarty->assign([
        'delivery_estimate' => $this->calculateDeliveryEstimate($product),
    ]);

    return $this->display(__FILE__, 'views/templates/hook/product-info.tpl');
}

// Hooks de acción: hacen el trabajo, el valor devuelto se descarta
public function hookActionCartSave($params)
{
    if (!isset($params['cart'])) {
        return;
    }
    $this->logCartActivity($params['cart']->id);
}

Qué hay dentro de $params

Depende del hook. Los cinco que verás con más frecuencia son $params['cart'], $params['order'], $params['product'], $params['customer'] y $params['cookie']. Si no estás seguro, regístralo en un log:

file_put_contents(
    _PS_ROOT_DIR_ . '/var/logs/hook_debug.log',
    date('c') . "\n" . print_r(array_keys($params), true) . "\n",
    FILE_APPEND
);

Elimina el registro de logs antes de publicar. Hemos publicado módulos con registros de depuración olvidados más de una vez, llenan var/logs/ en tiendas con mucho tráfico y consumen disco.

Encontrar un hook cuando no sabe cuál usar

La página de Posiciones en el back office de PrestaShop 8.

Esta página muestra qué módulos están registrados en qué hooks y en qué orden. Si un módulo no se dispara, este es el primer sitio que conviene revisar.

Página de Posiciones del back office de PrestaShop 8 que muestra los registros de hooks y el orden de los módulos

El modo de depuración le muestra la página

Parámetros Avanzados → Rendimiento → Modo de depuración → Sí. Recarga la página. PrestaShop anota cada punto de inserción de los hooks de visualización con su nombre. En PS 8+, la barra de herramientas de Symfony también lista todos los hooks disparados en la petición.

Importante: nunca actives el modo de depuración en producción. Expone trazas de pila, ralentiza el sitio y, en PS 9, también revelará las credenciales de la base de datos en las páginas de error.

La base de datos lo sabe

-- Todos los hooks que contienen "product" en el nombre
SELECT name, title FROM ps_hook WHERE name LIKE '%product%' ORDER BY name;

-- Qué módulos hay en un hook concreto, en orden de ejecución
SELECT m.name, hm.position
FROM ps_hook_module hm
JOIN ps_hook h ON h.id_hook = hm.id_hook
JOIN ps_module m ON m.id_module = hm.id_module
WHERE h.name = 'displayProductAdditionalInfo'
ORDER BY hm.position;

Hacer grep en el código fuente

# Hooks de visualización en las plantillas del tema
grep -rn "{hook " themes/your-theme/templates/

# Hooks de acción en el PHP del núcleo
grep -rn "Hook::exec" classes/ controllers/ src/

Para PS 8.x y 9.x, haz grep también en src/, los controladores Symfony también disparan hooks, y es fácil pasarlos por alto si solo miras el directorio heredado controllers/.

Crear su propio hook

Hazlo solo si tu módulo es en sí mismo un punto de extensión. Es decir, si quieres que otros módulos se conecten al tuyo. No crees hooks solo para organizar tu propio código; para eso están los métodos.

// En cualquier parte de tu módulo
$hookResult = Hook::exec('actionMyModuleBeforeProcess', [
    'order_id' => $orderId,
    'custom_data' => $myData,
]);

// Para los hooks de visualización, pasa null + true para obtener el HTML por módulo
$extraHtml = Hook::exec('displayMyModuleExtraContent', [
    'product' => $product,
], null, true);

Para que tu hook sea visible en Diseño → Posiciones, regístralo una vez:

$hook = new Hook();
$hook->name = 'displayMyModuleExtraContent';
$hook->title = 'My Module: Extra Content Area';
$hook->add();

El sistema de overrides: heredado pero no muerto

Los overrides permiten reemplazar métodos del núcleo sin editar archivos del núcleo. Eran la forma de personalizar PrestaShop en la era 1.5/1.6, antes de que los hooks cubrieran la mayoría de los casos de uso. Todavía funcionan (y aún los usamos, con moderación), pero tienen problemas reales que conviene entender antes de recurrir a uno.

Qué aspecto tiene un override

// override/classes/Cart.php
class Cart extends CartCore
{
    public function getOrderTotal($with_taxes = true, $type = Cart::BOTH,
        $products = null, $id_carrier = null, $use_cache = false)
    {
        $total = parent::getOrderTotal($with_taxes, $type, $products, $id_carrier, $use_cache);
        if ($this->hasSpecialProducts()) {
            $total += $this->calculateSurcharge();
        }
        return $total;
    }
}

El directorio de overrides refleja el del núcleo: override/classes/, override/controllers/front/, override/controllers/admin/. El autoloader de PrestaShop mira aquí primero, así que tu Cart ensombrece a CartCore.

Por qué evitamos los overrides cuando podemos

  • Entran en conflicto. Dos módulos no pueden sobrescribir el mismo método. El segundo en instalarse falla sin más. Esta es la razón número 1 de «dos módulos que quiero no funcionan juntos».
  • Se rompen al actualizar. Las firmas de los métodos cambian entre versiones de PrestaShop. Un override escrito para 1.7.7 puede provocar un error fatal en 1.7.8 y tumbar la tienda entera.
  • Son invisibles. El back office no da ninguna señal de que exista un override. Estás depurando un problema y no tienes ni idea de que la mitad de tu clase Cart no es la que crees.
  • El índice de clases puede corromperse. cache/class_index.php guarda en caché qué sobrescribe a qué. Cuando se desincroniza (normalmente tras una instalación de módulo fallida), la tienda muestra una pantalla en blanco hasta que se elimina el archivo.

El equipo del núcleo de PrestaShop lleva diciéndole a la gente que deje de escribir overrides desde 2017. Tienen razón. Usa uno solo cuando realmente no haya ningún hook, ningún servicio que decorar y ningún evento al que suscribirte.

Alternativas modernas (PS 8+)

Para PS 8 y 9, tres patrones reemplazan a la mayoría de los overrides:

Decoradores de servicio Symfony: envuelven un servicio del núcleo con su propia lógica. Varios módulos pueden decorar el mismo servicio sin conflictos:

# modules/mymodule/config/services.yml
services:
    mymodule.decorated_calculator:
        class: MyModule\Service\CalculatorDecorator
        decorates: 'prestashop.core.cart.calculator'
        arguments:
            - '@mymodule.decorated_calculator.inner'

Listeners de eventos de Doctrine: reaccionan a la persistencia de entidades sin crear subclases de ObjectModel:

# modules/mymodule/config/services.yml
services:
    mymodule.product_listener:
        class: MyModule\EventListener\ProductListener
        tags:
            - { name: doctrine.event_listener, event: postUpdate }

Manejadores de comandos/consultas CQRS: para las operaciones del back office, decora el bus de comandos. La curva de aprendizaje es la más pronunciada, pero ofrece la separación más limpia; esto es lo que usa internamente el código nuevo del núcleo de PrestaShop.

Cuando dos módulos chocan

Verás esto en var/logs/:

The method Cart::getOrderTotal is already overridden by module "othermodule".

La única solución real es la fusión manual: abre ambos archivos de override, combina la lógica en uno solo e instala el segundo módulo. Después de editar, elimina el índice de clases:

rm var/cache/prod/class_index.php
rm var/cache/dev/class_index.php

Esto es frágil. Cuando cualquiera de los dos módulos se actualice, tu fusión puede romperse. Documenta lo que has fusionado y guarda los archivos originales en algún sitio. Y si un módulo incluye un override donde un hook funcionaría, abre por favor un ticket de soporte con el desarrollador, la mayoría de nosotros cambiaremos a hooks si nos lo piden.

Posiciones, ordenación y trasplantes

Cuando varios módulos se registran en el mismo hook de visualización, la posición determina el orden de renderizado. El comerciante puede arrastrarlos y soltarlos en Diseño → Posiciones, desenganchar lo que no quiera o trasplantar un módulo de un hook a otro.

El trasplante funciona en los casos sencillos. No funciona siempre, y los módulos que asumen un hook concreto (porque leen el contexto del controlador, esperan ciertos $params o usan variables Smarty específicas del hook) se renderizarán mal o no se renderizarán en absoluto. Si el comerciante se queja de que «el módulo desapareció cuando lo moví», por eso es.

También puedes fijar la posición de forma programática en la instalación:

// En install(): mover a la posición 1
$this->updatePosition($this->getHookId('displayHome'), false, 1);

Depurar cuando un hook no se dispara

Recorre estos pasos en orden. La causa está casi siempre en los dos primeros.

-- 1. ¿Está el módulo registrado en el hook?
SELECT m.name, hm.position
FROM ps_hook_module hm
JOIN ps_hook h ON h.id_hook = hm.id_hook
JOIN ps_module m ON m.id_module = hm.id_module
WHERE h.name = 'displayProductAdditionalInfo'
  AND m.name = 'your_module_name';

-- 2. ¿Está el módulo activo e instalado?
SELECT name, active FROM ps_module WHERE name = 'your_module_name';

Si ambas filas existen y el método sigue sin dispararse, comprueba:

SíntomaCausa más probableSolución
El método se llama, pero no devuelve nadaRuta de la plantilla incorrecta, o falta el return en un hook de visualizaciónActiva la depuración, comprueba la ruta de la plantilla, confirma que has devuelto el resultado renderizado
Se dispara en todas las páginas cuando solo quieres las de productoNo hay comprobación del controladorif (!($this->context->controller instanceof ProductController)) return '';
Se dispara dos veces en la misma páginaRegistrado dos veces en ps_hook_moduleDELETE la fila duplicada, o reinicia el módulo
$params['product'] es un array, no un objetoPS 1.7+ usa arrays de presentador en el front officeUsa $params['product']['id_product'], o llama a new Product($id) si necesitas el objeto completo
Funciona en desarrollo, falla en producciónPlantillas Smarty compiladas en cachéForzar compilación + borrar caché en Parámetros Avanzados → Rendimiento

Perfilado de hooks

Activa el perfilado para ver el tiempo de cada hook:

// PS 1.7: defines.inc.php
define('_PS_DEBUG_PROFILING_', true);

// PS 8+: .env.local
APP_DEBUG=1
APP_ENV=dev

Qué cambió en PrestaShop 9

Si estás migrando un módulo, esta es la parte que conviene leer con atención.

Hooks nuevos

PS 9 añade hooks en lugares que antes requerían overrides: extensiones del formulario de productos del back office, aplicación de reglas de descuento del carrito, operaciones de recursos de la API y envío de correos. Cada override que eliminamos durante nuestra migración a PS 9 fue reemplazado por uno de estos.

Hooks obsoletos

Los hooks ligados a los controladores heredados del back office están obsoletos a medida que esos controladores migran a Symfony. La lista de obsolescencia vive en _PS_DEPRECATED_HOOKS_, búscala en el archivo de constantes del núcleo. Tu módulo seguirá funcionando, pero cada carga de página registrará una advertencia de obsolescencia, y el hook se eliminará en una futura versión menor.

Los overrides son cada vez menos útiles, pero no han desaparecido

Los overrides siguen funcionando para las clases heredadas de ObjectModel. No funcionan para los servicios Symfony ni para los nuevos controladores del back office. No hay nada de lo que crear una subclase. A medida que más partes del back office migran a Symfony, la cobertura de los overrides se reduce. Tratamos cada nuevo override que escribimos como deuda técnica con una fecha de eliminación conocida.

Referencia rápida

// Registrar uno o varios
$this->registerHook(['displayHeader', 'actionCartSave']);

// ¿Existe este hook?
$hookId = Hook::getIdByName('displayMyCustomHook');

// ¿Qué módulos hay en este hook?
$modules = Hook::getHookModuleExecList('displayHeader');

// Disparar un hook personalizado
$output = Hook::exec('displayMyHook', ['key' => 'value']);

// Renderizar solo en las páginas de producto
public function hookDisplayHeader($params) {
    if (!($this->context->controller instanceof ProductController)) {
        return '';
    }
    return $this->display(__FILE__, 'views/templates/hook/header.tpl');
}
-- Todos los hooks en los que está un módulo dado
SELECT h.name, hm.position
FROM ps_hook_module hm
JOIN ps_hook h ON h.id_hook = hm.id_hook
JOIN ps_module m ON m.id_module = hm.id_module
WHERE m.name = 'your_module_name'
ORDER BY h.name;

-- Hooks sin módulos registrados (hooks potencialmente abandonados)
SELECT h.name FROM ps_hook h
LEFT JOIN ps_hook_module hm ON h.id_hook = hm.id_hook
WHERE hm.id_hook IS NULL
ORDER BY h.name;

Lecturas relacionadas

Cargando...
Volver arriba