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:displayProductAdditionalInfoadds content below the product price. - Action hooks (prefixed with
action) — Event notifications. They don't produce output — they tell modules something happened. Example:actionCartSavefires 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 Name | When It Fires | Typical Use Case |
|---|---|---|
displayHeader | Inside <head> on every page | Meta tags, tracking scripts |
displayFooter | Page footer | Footer widgets, analytics |
displayHome | Homepage content area | Banners, featured products |
displayProductAdditionalInfo | Below "Add to Cart" | Delivery estimates, size guides |
displayShoppingCart | Cart summary page | Cross-sells, shipping estimators |
displayOrderConfirmation | After order placement | Conversion tracking |
displayCustomerAccount | "My Account" page | Custom account sections |
displayBanner | Top of page, above header | Sale announcements |
Back Office Display Hooks
| Hook Name | When It Fires | Typical Use Case |
|---|---|---|
displayBackOfficeHeader | Admin <head> | Admin CSS/JS, notifications |
displayAdminOrder | Order detail page | Custom panels, shipping integrations |
displayAdminProductsExtra | Product editor tab | Custom product fields |
Action Hooks
| Hook Name | When It Fires | Typical Use Case |
|---|---|---|
actionCartSave | Cart created or updated | Inventory reservation, analytics |
actionOrderStatusUpdate | Order status changes | Notifications, ERP sync |
actionProductUpdate | Product saved in admin | External catalog sync |
actionCustomerAccountAdd | New customer registers | Welcome email, CRM sync |
actionValidateOrder | Order validated | Payment processing, stock update |
actionFrontControllerSetMedia | Front controller loads assets | Register CSS/JS |
actionAdminControllerSetMedia | Admin controller loads assets | Register 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
| Scenario | Approach |
|---|---|
| Add content to a page | Display hook |
| React to an event | Action hook |
| Add CSS/JS | Asset registration hook |
| Change a core calculation | Override (if no hook exists) |
| Modify a core SQL query | Override (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.phpmapping 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
| Symptom | Cause | Fix |
|---|---|---|
| Method never called | Not registered on hook | Check ps_hook_module; reset module |
| Returns empty string | Template not found | Check template path, enable errors |
| Fires on wrong pages | No page check | Add instanceof ProductController |
| Fires twice | Registered twice | Check for duplicate rows |
| $params['product'] is array | PS 1.7+ uses arrays | Use $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.