Dove agganciarsi in PrestaShop e quando conviene un override
Riferimento su hook e override PrestaShop: display hook, action hook, hook personalizzati, sistema di override e migrazione a pattern moderni.
Cosa fanno davvero gli hook (e dove la maggior parte delle persone si blocca)
Ogni modulo che distribuiamo da mypresta.rocks si collega a PrestaShop tramite gli hook, e dopo averne portati 159 in produzione, possiamo dirti che la maggior parte dei bug sugli hook che abbiamo corretto dal 2013 risale sempre alla stessa manciata di errori: registrato sull'hook sbagliato, non registrato da nessuna parte, eseguito due volte, oppure eseguito dove lo sviluppatore presumeva ci fosse un override.
È anche la pagina che inviamo ai clienti quando ci chiedono "perché questo modulo richiede un aggiornamento per PrestaShop 9?", perché la risposta è quasi sempre "l'hook che usava è cambiato".
Due tipi di hook, e l'unica regola che conta
PrestaShop ha gli hook di visualizzazione (chiedono al tuo modulo dell'HTML da renderizzare) e gli hook d'azione (comunicano al tuo modulo che è successo qualcosa (ordine effettuato, carrello salvato, cliente registrato) e ignorano qualunque cosa tu restituisca).
La regola che coglie tutti in fallo: il valore di ritorno conta per gli hook di visualizzazione, non conta per gli hook d'azione. Se tu esegui return $this->display(...) da un hook d'azione, non si rompe nulla. Se dimentichi di fare il return da un hook di visualizzazione, il tuo modulo non renderizza nulla e perderai un'ora a chiederti perché.
Come PrestaShop chiama davvero il suo codice
Quando il core raggiunge un punto di hook, Hook::exec() interroga ps_hook_module, ordina i moduli attivi per posizione, chiama il metodo hookXxx() di ciascun modulo con un array $params e (per gli hook di visualizzazione) concatena l'HTML restituito.
Tutto qui. Nessuna magia. Il motivo per cui il tuo hook non viene eseguito è quasi sempre una riga mancante in ps_hook_module, non qualcosa che non va nel tuo metodo.
I quattro pattern usati dalla maggior parte dei moduli
Prima di consultare la documentazione, chiediti quale di questi quattro compiti stai svolgendo. Scegli il pattern sbagliato e combatterai contro il framework per tutta la vita del modulo.
| Hai bisogno di… | Affidati a… | Non perdere tempo con… |
|---|---|---|
| Renderizzare HTML in un tema o in una pagina di amministrazione | Un hook display* che restituisce un template Smarty | Fare echo all'interno di displayHeader |
| Reagire a un cambiamento di stato (ordine effettuato, carrello salvato, prodotto aggiornato) | Un hook action* | Un override del controller |
| Aggiungere CSS o JS al front office | actionFrontControllerSetMedia + registerStylesheet / registerJavascript | Tag <link> grezzi tramite displayHeader, annulla il CCC e rompe il versioning degli asset |
| Modificare un calcolo del core privo di hook | Un service decorator Symfony (PS 8+) o, come ultima risorsa, un override | Modificare i file del core |
Gli hook che userà davvero
PrestaShop offre centinaia di hook. Noi ci registriamo su circa una dozzina di essi in tutto il nostro catalogo di moduli. Questi sono quelli che vale la pena memorizzare.
Front office: visualizzazione
| Hook | Si attiva | Da usare per |
|---|---|---|
displayProductAdditionalInfo | Sotto il pulsante Aggiungi al carrello | Stime di consegna, guide alle taglie, contenuti extra nella pagina prodotto: lo spazio più prezioso di una pagina prodotto |
displayShoppingCart | Pagina di riepilogo del carrello | Cross-sell, calcolatori di spedizione, messaggi di urgenza |
displayOrderConfirmation | Pagina di ringraziamento dopo il checkout | Tracciamento delle conversioni, upsell post-acquisto, inviti alla segnalazione |
displayCustomerAccount | Pagina "Il mio account" | Sezioni personalizzate, liste dei desideri, punti fedeltà, richieste di reso (RMA) |
displayBanner | In cima alla pagina, sopra l'header | Strisce promozionali, barre di spedizione gratuita, avvisi GDPR |
displayHome | Area dei contenuti della homepage | Collezioni in evidenza, moduli hero, blocchi HTML riutilizzabili |
Back office: visualizzazione
| Hook | Si attiva | Da usare per |
|---|---|---|
displayAdminOrder | Pagina di dettaglio dell'ordine | Pannelli personalizzati, integrazioni di spedizione, stato della sincronizzazione ERP, segnali di frode |
displayAdminProductsExtra | Nuova scheda nell'editor del prodotto | Campi prodotto personalizzati, dati di terze parti |
displayBackOfficeHeader | All'interno dell'<head> dell'amministrazione | CSS/JS dell'amministrazione, widget della dashboard, avvisi di aggiornamento |
Hook d'azione
| Hook | Si attiva | Da usare per |
|---|---|---|
actionValidateOrder | Ordine convalidato con successo | Conferma del pagamento, decremento delle scorte, flusso di lavoro post-pagamento, l'hook d'azione più importante |
actionOrderStatusUpdate | Lo stato dell'ordine cambia | Notifiche al corriere, sincronizzazione ERP, email ai clienti |
actionCartSave | Carrello creato o aggiornato | Trigger per carrelli abbandonati, prenotazione delle scorte, analitica |
actionProductUpdate | Prodotto salvato in amministrazione | Sincronizzazione di cataloghi esterni, reindicizzazione della ricerca, rigenerazione delle immagini |
actionCustomerAccountAdd | Si registra un nuovo cliente | Email di benvenuto, sincronizzazione CRM, segmentazione |
actionFrontControllerSetMedia | Il front controller carica gli asset | Registrare CSS/JS nel modo corretto |
L'hook per gli asset: la prego di usare questo, non displayHeader
Da PrestaShop 1.7, l'unico punto corretto in cui aggiungere CSS o JS al front office è 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]
);
}
I moduli che ancora iniettano tag <link> e <script> grezzi tramite displayHeader rompono Combine-Compress-Cache (CCC), aggirano la stringa di query per il cache-busting e non possono essere differiti. Ogni volta che facciamo l'audit di un negozio lento, almeno due moduli si comportano così. Non essere uno di questi.
Collegare un hook al suo modulo
Registrare durante l'installazione
public function install()
{
return parent::install()
&& $this->registerHook('displayProductAdditionalInfo')
&& $this->registerHook('actionCartSave')
&& $this->registerHook('actionFrontControllerSetMedia');
}
Ogni chiamata a registerHook() inserisce una riga in ps_hook_module. Se dimentichi di registrare, il tuo metodo hook è come se non esistesse, PrestaShop non lo chiamerà mai. Questa è la causa più comune di "il mio modulo non funziona" che vediamo nel supporto.
Un'altra trappola: se aggiungi un nuovo hook nella versione 1.2 del tuo modulo dopo aver distribuito la 1.0, le installazioni dei clienti esistenti non ricevono la registrazione. Ti serve uno script di aggiornamento (upgrade/install-1.2.0.php) che chiami registerHook(), oppure istruzioni per reimpostare il modulo. Inserisci questa informazione nelle note di rilascio, altrimenti riceverai ticket.
Implementare il metodo
Il nome del metodo è letteralmente hook + il nome dell'hook, con la prima lettera maiuscola:
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');
}
// Hook d'azione: svolgono il lavoro, il valore di ritorno viene scartato
public function hookActionCartSave($params)
{
if (!isset($params['cart'])) {
return;
}
$this->logCartActivity($params['cart']->id);
}
Cosa contiene $params
Dipende dall'hook. I cinque che vedrai più spesso sono $params['cart'], $params['order'], $params['product'], $params['customer'] e $params['cookie']. Se non sei sicuro, registralo nel log:
file_put_contents(
_PS_ROOT_DIR_ . '/var/logs/hook_debug.log',
date('c') . "\n" . print_r(array_keys($params), true) . "\n",
FILE_APPEND
);
Rimuovi il logging prima di distribuire. A noi è capitato più di una volta di distribuire moduli con logging di debug lasciato attivo, riempie var/logs/ sui negozi molto trafficati e consuma spazio su disco.
Trovare un hook quando non sa quale usare
La pagina Posizioni nel Back Office di PrestaShop 8.
Questa pagina mostra quali moduli sono registrati su quali hook e in quale ordine. Se un modulo non si attiva, questo è il primo posto da controllare.
La modalità di debug le mostra la pagina
Parametri Avanzati → Prestazioni → Modalità di debug → Sì. Ricarica la pagina. PrestaShop annota ogni punto di inserimento degli hook di visualizzazione con il relativo nome. In PS 8+ anche la barra degli strumenti di Symfony elenca ogni hook attivato nella richiesta.
Importante: non attivare mai la modalità di debug in produzione. Espone gli stack trace, rallenta il sito e, su PS 9, rivela anche le credenziali del database nelle pagine di errore.
Il database lo sa
-- Tutti gli hook con "product" nel nome
SELECT name, title FROM ps_hook WHERE name LIKE '%product%' ORDER BY name;
-- Quali moduli sono su un hook specifico, in ordine di esecuzione
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;
Faccia grep del codice sorgente
# Hook di visualizzazione nei template del tema
grep -rn "{hook " themes/your-theme/templates/
# Hook d'azione nel PHP del core
grep -rn "Hook::exec" classes/ controllers/ src/
Per PS 8.x e 9.x, fai grep anche di src/, anche i controller Symfony attivano hook, ed è facile non vederli se si guarda solo nella directory legacy controllers/.
Creare il proprio hook
Fallo solo se il tuo modulo è esso stesso un punto di estensione, ovvero se vuoi che altri moduli si colleghino al tuo. Non creare hook solo per organizzare il tuo codice; a questo servono i metodi.
// In un punto qualsiasi del tuo modulo
$hookResult = Hook::exec('actionMyModuleBeforeProcess', [
'order_id' => $orderId,
'custom_data' => $myData,
]);
// Per gli hook di visualizzazione, passa null + true per ottenere l'HTML per ciascun modulo
$extraHtml = Hook::exec('displayMyModuleExtraContent', [
'product' => $product,
], null, true);
Per rendere il tuo hook visibile in Design → Posizioni, registralo una volta:
$hook = new Hook();
$hook->name = 'displayMyModuleExtraContent';
$hook->title = 'My Module: Extra Content Area';
$hook->add();
Il sistema di override: legacy ma non morto
Gli override consentono di sostituire i metodi del core senza modificare i file del core. Erano il modo in cui si personalizzava PrestaShop nell'era 1.5/1.6, prima che gli hook coprissero la maggior parte dei casi d'uso. Funzionano ancora (e li usiamo ancora, con parsimonia) ma hanno problemi concreti che dovresti comprendere prima di ricorrervi.
Com'è fatto 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;
}
}
La directory degli override rispecchia quella del core: override/classes/, override/controllers/front/, override/controllers/admin/. L'autoloader di PrestaShop guarda prima qui, quindi la tua classe Cart mette in ombra CartCore.
Perché evitiamo gli override quando possiamo
- Entrano in conflitto. Due moduli non possono fare l'override dello stesso metodo. Il secondo che viene installato fallisce del tutto. Questo è il motivo numero 1 per cui "due moduli che voglio non funzionano insieme".
- Si rompono durante l'aggiornamento. Le firme dei metodi cambiano tra le versioni di PrestaShop. Un override scritto per la 1.7.7 può mandare in crash la 1.7.8 con un errore fatale e far cadere l'intero negozio.
- Sono invisibili. Il Back Office non mostra alcun segno dell'esistenza di un override. Tu stai facendo il debug di un problema senza sapere che metà della tua classe
Cartnon è quella che credi. - L'indice delle classi può degradarsi.
cache/class_index.phpmemorizza nella cache cosa fa l'override di cosa. Quando si desincronizza (di solito dopo un'installazione di modulo andata storta) il negozio mostra una schermata bianca finché non si elimina il file.
Il team del core di PrestaShop dice alle persone di smettere di scrivere override dal 2017. Hanno ragione. Usane uno solo quando davvero non esiste un hook, né un servizio da decorare, né un evento da ascoltare.
Alternative moderne (PS 8+)
Per PS 8 e 9, tre schemi sostituiscono la maggior parte degli override:
Decoratori di servizi Symfony: avvolgi un servizio del core con la tua logica. Più moduli possono decorare lo stesso servizio senza conflitti:
# modules/mymodule/config/services.yml
services:
mymodule.decorated_calculator:
class: MyModule\Service\CalculatorDecorator
decorates: 'prestashop.core.cart.calculator'
arguments:
- '@mymodule.decorated_calculator.inner'
Listener di eventi Doctrine: reagiscono alla persistenza delle entità senza estendere ObjectModel:
# modules/mymodule/config/services.yml
services:
mymodule.product_listener:
class: MyModule\EventListener\ProductListener
tags:
- { name: doctrine.event_listener, event: postUpdate }
Gestori di comandi/query CQRS: per le operazioni del back office, decorano il bus dei comandi. La curva di apprendimento più ripida, ma la separazione più pulita; è ciò che il nuovo codice del core di PrestaShop usa internamente.
Quando due moduli si scontrano
Lo vedrai in var/logs/:
The method Cart::getOrderTotal is already overridden by module "othermodule".
L'unica soluzione reale è la fusione manuale: apri entrambi i file di override, combina la logica in uno solo, installa il secondo modulo. Dopo la modifica, elimina l'indice delle classi:
rm var/cache/prod/class_index.php
rm var/cache/dev/class_index.php
Questo è fragile. Quando uno dei due moduli viene aggiornato, la tua fusione può rompersi. Documenta ciò che hai unito e conserva i file originali da qualche parte. E se un modulo distribuisce un override dove un hook funzionerebbe, ti prego di aprire un ticket di supporto con lo sviluppatore, la maggior parte di noi passerà agli hook se richiesto.
Posizioni, ordinamento e trapianti
Quando più moduli si registrano sullo stesso hook di visualizzazione, la posizione determina l'ordine di rendering. Il commerciante può trascinarli in Design → Posizioni, scollegare ciò che non desidera, oppure trapiantare un modulo da un hook a un altro.
Il trapianto funziona nei casi semplici. Non funziona sempre, e i moduli che presuppongono un hook specifico (perché leggono il contesto del controller, si aspettano determinati $params o usano variabili Smarty specifiche dell'hook) verranno renderizzati in modo errato o non verranno renderizzati affatto. Se il commerciante si lamenta che "il modulo è sparito quando l'ho spostato", il motivo è questo.
Puoi anche impostare la posizione in modo programmatico durante l'installazione:
// In install(): spostamento alla posizione 1
$this->updatePosition($this->getHookId('displayHome'), false, 1);
Fare il debug quando un hook non si attiva
Procedi con questi controlli in ordine. La causa è quasi sempre nei primi due.
-- 1. Il modulo è registrato sull'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. Il modulo è attivo e installato?
SELECT name, active FROM ps_module WHERE name = 'your_module_name';
Se entrambe le righe esistono e il metodo continua a non attivarsi, controlla:
| Sintomo | Causa più probabile | Soluzione |
|---|---|---|
| Il metodo viene chiamato ma non restituisce nulla | Percorso del template errato, oppure return mancante su un hook di visualizzazione | Attiva il debug, controlla il percorso del template, conferma di aver restituito l'output renderizzato |
| Si attiva su ogni pagina mentre vuoi solo le pagine prodotto | Nessun controllo sul controller | if (!($this->context->controller instanceof ProductController)) return ''; |
| Si attiva due volte sulla stessa pagina | Registrato due volte in ps_hook_module | Esegui DELETE della riga duplicata, oppure reimposta il modulo |
$params['product'] è un array e non un oggetto | PS 1.7+ usa gli array del presenter nel front office | Usa $params['product']['id_product'], oppure chiama new Product($id) se ti serve l'oggetto completo |
| Funziona in dev, si rompe in prod | Template Smarty compilati e memorizzati nella cache | Forza la compilazione + svuota la cache in Parametri Avanzati → Prestazioni |
Profiling degli hook
Attiva il profiling per vedere i tempi per ciascun hook:
// PS 1.7: defines.inc.php
define('_PS_DEBUG_PROFILING_', true);
// PS 8+: .env.local
APP_DEBUG=1
APP_ENV=dev
Cosa è cambiato in PrestaShop 9
Se stai migrando un modulo, questa è la parte da leggere con attenzione.
Nuovi hook
PS 9 aggiunge hook in punti che prima richiedevano gli override: estensioni del form prodotto in amministrazione, applicazione delle regole di prezzo sul carrello, operazioni sulle risorse API e invio delle email. Ogni override rimosso durante la nostra migrazione a PS 9 è stato sostituito da uno di questi.
Hook deprecati
Gli hook legati ai controller di amministrazione legacy sono deprecati man mano che questi controller migrano a Symfony. La lista delle deprecazioni si trova in _PS_DEPRECATED_HOOKS_, cercala nel file delle costanti del core. Il tuo modulo continuerà a funzionare, ma ogni caricamento di pagina registrerà un avviso di deprecazione, e l'hook verrà rimosso in una futura versione minore.
Gli override diventano meno utili, non scompaiono
Gli override funzionano ancora per le classi ObjectModel legacy. Non funzionano per i servizi Symfony o per i nuovi controller di amministrazione, non c'è nulla da estendere. Man mano che una parte sempre maggiore del back office migra a Symfony, la copertura degli override si riduce. Noi trattiamo ogni nuovo override che scriviamo come debito tecnico con una data di rimozione nota.
Riferimento rapido
// Registrazione di uno o più hook
$this->registerHook(['displayHeader', 'actionCartSave']);
// Questo hook esiste?
$hookId = Hook::getIdByName('displayMyCustomHook');
// Quali moduli sono su questo hook?
$modules = Hook::getHookModuleExecList('displayHeader');
// Attivazione di un hook personalizzato
$output = Hook::exec('displayMyHook', ['key' => 'value']);
// Rendering solo sulle pagine prodotto
public function hookDisplayHeader($params) {
if (!($this->context->controller instanceof ProductController)) {
return '';
}
return $this->display(__FILE__, 'views/templates/hook/header.tpl');
}
-- Tutti gli hook su cui è registrato un dato modulo
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;
-- Hook senza alcun modulo registrato (hook potenzialmente abbandonati)
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;
Letture correlate
- Come migrare a PrestaShop 9: guida completa all'aggiornamento: quali hook sono stati aggiunti e deprecati in PS 9
- Temi figlio di PrestaShop: guida alla personalizzazione di Classic e Hummingbird: per le modifiche a livello di template, preferisci un tema figlio a un override
- Strumenti essenziali per lo sviluppo PrestaShop: gli strumenti di debug che usiamo ogni giorno
- Padroneggiare gli hook di PrestaShop: un riferimento per sviluppatori per 1.7, 8.x e 9.x: approfondimento complementare con più esempi
- Checkout Revolution e Performance Revolution: due dei nostri moduli che si appoggiano molto agli hook; utili come riferimenti di produzione