Knowledge Base Guide

PrestaShop Hooks & Overrides: Developer Reference Guide

Developer reference for PrestaShop hooks and overrides — display hooks, action hooks, custom hooks, the override system, and migration to modern patterns.

What Are Hooks and Why They Matter

Hooks are PrestaShop's extension system — the mechanism by which modules interact with the core platform without modifying core files. Every module you install relies on hooks to plug into the right place at the right time.

If you've ever wondered how a module "knows" to display something below the product price, or how it reacts when a customer places an order — the answer is hooks.

Hooks are to PrestaShop what electrical outlets are to a house. The core provides outlets in specific locations, and modules plug into them. You don't rewire the house — you plug in where you need power.

Display Hooks vs Action Hooks

PrestaShop has two types of hooks:

  • Display hooks (prefixed with display) — Visual insertion points. When fired, they collect HTML from registered modules and render it. Example: displayProductAdditionalInfo adds content below the product price.
  • Action hooks (prefixed with action) — Event notifications. They don't produce output — they tell modules something happened. Example: actionCartSave fires after a cart is updated.

How the Hook Dispatcher Works

When PrestaShop reaches a hook point, it calls Hook::exec(), which looks up registered modules in ps_hook_module, sorts by position, calls each module's hook method with a $params array, and for display hooks concatenates the returned HTML.

Essential Hooks Every Developer Should Know

Front Office Display Hooks

Hook NameWhen It FiresTypical Use Case
displayHeaderInside <head> on every pageMeta tags, tracking scripts
displayFooterPage footerFooter widgets, analytics
displayHomeHomepage content areaBanners, featured products
displayProductAdditionalInfoBelow "Add to Cart"Delivery estimates, size guides
displayShoppingCartCart summary pageCross-sells, shipping estimators
displayOrderConfirmationAfter order placementConversion tracking
displayCustomerAccount"My Account" pageCustom account sections
displayBannerTop of page, above headerSale announcements

Back Office Display Hooks

Hook NameWhen It FiresTypical Use Case
displayBackOfficeHeaderAdmin <head>Admin CSS/JS, notifications
displayAdminOrderOrder detail pageCustom panels, shipping integrations
displayAdminProductsExtraProduct editor tabCustom product fields

Action Hooks

Hook NameWhen It FiresTypical Use Case
actionCartSaveCart created or updatedInventory reservation, analytics
actionOrderStatusUpdateOrder status changesNotifications, ERP sync
actionProductUpdateProduct saved in adminExternal catalog sync
actionCustomerAccountAddNew customer registersWelcome email, CRM sync
actionValidateOrderOrder validatedPayment processing, stock update
actionFrontControllerSetMediaFront controller loads assetsRegister CSS/JS
actionAdminControllerSetMediaAdmin controller loads assetsRegister admin CSS/JS

Asset Registration

Since PS 1.7, the proper way to add CSS/JS is through asset registration inside 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]
    );
}

This enables CCC (Combine, Compress, Cache) optimization — far better than injecting raw tags through displayHeader.

How to Use Hooks in a Module

Register Hooks During Installation

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

Each call inserts a row into ps_hook_module. If you forget to register, the hook method will never fire — even if the method exists.

Common debugging headache: the hook method exists and is correctly named, but the module never registered on the hook during install. If you add a hook after initial release, you need an upgrade script or module reset.

Implement the Hook Method

Method naming convention: hook + hook name with first letter capitalized.

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');
}

// Action hook — no return value
public function hookActionCartSave($params)
{
    if (!isset($params['cart'])) {
        return;
    }
    $this->logCartActivity($params['cart']->id);
}

The $params Array

Contents vary by hook. Common ones: $params['cart'], $params['order'], $params['product'], $params['customer']. To see what a hook passes, add temporary logging:

file_put_contents(
    _PS_ROOT_DIR_ . '/var/logs/hook_debug.log',
    print_r(array_keys($params), true),
    FILE_APPEND
);

Finding Available Hooks

1. Debug Mode

Enable at Advanced Parameters → Performance → Debug mode: YES. The front office will show hook names at insertion points. In PS 8+, the Symfony debug toolbar shows all hooks fired per request.

Never enable debug mode on a production store. It exposes internal data and slows down the site.

2. Database Query

-- All hooks with "product" in the name
SELECT name, title FROM ps_hook WHERE name LIKE '%product%' ORDER BY name;

-- Which modules are on a specific 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'
ORDER BY hm.position;

3. Search Templates and Source Code

# Display hooks in theme templates
grep -rn "{hook " themes/your-theme/templates/

# Action hooks in PHP core
grep -rn "Hook::exec" classes/ controllers/ src/

Creating Custom Hooks

When built-in hooks don't fire at the exact point you need, create your own:

// Dispatch a custom hook
$hookResult = Hook::exec('actionMyModuleBeforeProcess', [
    'order_id' => $orderId,
    'custom_data' => $myData,
]);

// For display hooks — true returns array per module
$extraHtml = Hook::exec('displayMyModuleExtraContent', [
    'product' => $product,
], null, true);

Register it in install() as you would any hook. To give it a friendly name in Positions:

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

Create custom hooks when your module provides extension points other modules need. Don't create them for internal-only logic — use regular methods.

The Override System

Overrides replace core classes and controllers without modifying core files. They were the primary customization method in PS 1.5/1.6, before hooks were comprehensive.

How Overrides Work

An override extends a core class and replaces specific methods. The autoloader checks override/ first:

// 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;
    }
}

The directory structure mirrors the core: override/classes/, override/controllers/front/, override/controllers/admin/.

Override vs Hook

ScenarioApproach
Add content to a pageDisplay hook
React to an eventAction hook
Add CSS/JSAsset registration hook
Change a core calculationOverride (if no hook exists)
Modify a core SQL queryOverride (if no hook exists)

The Problems with Overrides

  • Conflicts: Two modules cannot override the same method. The second one fails to install.
  • Upgrade breakage: Core method signatures change between versions. An override for PS 1.7.7 may crash on 1.7.8.
  • Hidden behavior: Overrides are invisible from Back Office, making debugging harder.
  • Index corruption: The class_index.php mapping can become corrupted, breaking the entire store.
The PrestaShop core team has been actively discouraging overrides since version 1.7. If you find yourself reaching for an override, first check if a hook or Symfony-based alternative exists.

Override Alternatives (PS 8+)

Symfony Service Decorators

Wrap core services with your own logic — multiple decorators can chain without conflicts:

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

Doctrine Event Listeners

React to entity changes without overriding ObjectModel:

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

CQRS Commands and Queries

Hook into admin operations through command/query handlers. The most modern approach, requiring familiarity with PrestaShop's Symfony architecture.

Resolving Override Conflicts

When two modules override the same method, you'll see:

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

The fix is manual merging — combine both modules' logic into a single override file. After editing, rebuild the class index:

# Delete class index — PrestaShop rebuilds automatically
rm var/cache/prod/class_index.php
rm var/cache/dev/class_index.php
Manual override merging is fragile. When either module updates, you may need to re-merge. Document what you merged and why.

If a module uses an override where a hook would work, contact the developer and request a hook-based solution.

Hook Priority and Execution Order

When multiple modules register on the same display hook, position determines order. Manage this at Design → Positions where you can drag-drop modules, unhook them, or transplant them to different hooks.

Transplanting

Move a module from one hook to another via Design → Positions → Transplant a Module. Not all transplants render correctly — it depends on the module's templates.

Programmatic Positioning

// In install() — move to position 1
$this->updatePosition($this->getHookId('displayHome'), false, 1);

Debugging Hooks

Systematic Checks

-- Is the module registered on the 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';

-- Is the module active?
SELECT name, active FROM ps_module WHERE name = 'your_module_name';

Common Problems

SymptomCauseFix
Method never calledNot registered on hookCheck ps_hook_module; reset module
Returns empty stringTemplate not foundCheck template path, enable errors
Fires on wrong pagesNo page checkAdd instanceof ProductController
Fires twiceRegistered twiceCheck for duplicate rows
$params['product'] is arrayPS 1.7+ uses arraysUse $params['product']['id_product']

Hook Profiling

Enable profiling to see timing per hook:

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

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

PrestaShop 9 Changes

New Hooks

PS 9 adds hooks in areas that previously required overrides: admin product form extensions, cart price rule application, API resource operations, and email sending process.

Deprecated Hooks

Hooks tied to legacy admin controllers are deprecated as those controllers migrate to Symfony. Check _PS_DEPRECATED_HOOKS_ for the full list.

Override System Future

Overrides still work for legacy ObjectModel classes, but new Symfony services and admin controllers cannot use them. The override system will eventually be removed as the Symfony migration completes.

If you're starting a new module today, build exclusively with hooks and Symfony services. Every override you write now is technical debt you'll remove later.

Quick Reference

// Register hooks (install)
$this->registerHook(['displayHeader', 'actionCartSave']);

// Check if hook exists
$hookId = Hook::getIdByName('displayMyCustomHook');

// Get modules on a hook
$modules = Hook::getHookModuleExecList('displayHeader');

// Execute hook manually
$output = Hook::exec('displayMyHook', ['key' => 'value']);

// Conditional execution
public function hookDisplayHeader($params) {
    if (!($this->context->controller instanceof ProductController)) return '';
    return $this->display(__FILE__, 'views/templates/hook/header.tpl');
}
-- All hooks a module is registered on
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 with no modules registered
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;

Master hooks first — they cover the vast majority of use cases. Learn overrides to understand legacy code and resolve conflicts. For PrestaShop 8+, invest in Symfony-based alternatives. The ecosystem is moving forward, and hooks are leading the way.

More guides available

Browse our knowledge base for more practical PrestaShop tutorials, or reach out if you need help.

Loading...
Back to top