Guida

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 amministrazioneUn hook display* che restituisce un template SmartyFare 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 officeactionFrontControllerSetMedia + registerStylesheet / registerJavascriptTag <link> grezzi tramite displayHeader, annulla il CCC e rompe il versioning degli asset
Modificare un calcolo del core privo di hookUn service decorator Symfony (PS 8+) o, come ultima risorsa, un overrideModificare 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

HookSi attivaDa usare per
displayProductAdditionalInfoSotto il pulsante Aggiungi al carrelloStime di consegna, guide alle taglie, contenuti extra nella pagina prodotto: lo spazio più prezioso di una pagina prodotto
displayShoppingCartPagina di riepilogo del carrelloCross-sell, calcolatori di spedizione, messaggi di urgenza
displayOrderConfirmationPagina di ringraziamento dopo il checkoutTracciamento delle conversioni, upsell post-acquisto, inviti alla segnalazione
displayCustomerAccountPagina "Il mio account"Sezioni personalizzate, liste dei desideri, punti fedeltà, richieste di reso (RMA)
displayBannerIn cima alla pagina, sopra l'headerStrisce promozionali, barre di spedizione gratuita, avvisi GDPR
displayHomeArea dei contenuti della homepageCollezioni in evidenza, moduli hero, blocchi HTML riutilizzabili

Back office: visualizzazione

HookSi attivaDa usare per
displayAdminOrderPagina di dettaglio dell'ordinePannelli personalizzati, integrazioni di spedizione, stato della sincronizzazione ERP, segnali di frode
displayAdminProductsExtraNuova scheda nell'editor del prodottoCampi prodotto personalizzati, dati di terze parti
displayBackOfficeHeaderAll'interno dell'<head> dell'amministrazioneCSS/JS dell'amministrazione, widget della dashboard, avvisi di aggiornamento

Hook d'azione

HookSi attivaDa usare per
actionValidateOrderOrdine convalidato con successoConferma del pagamento, decremento delle scorte, flusso di lavoro post-pagamento, l'hook d'azione più importante
actionOrderStatusUpdateLo stato dell'ordine cambiaNotifiche al corriere, sincronizzazione ERP, email ai clienti
actionCartSaveCarrello creato o aggiornatoTrigger per carrelli abbandonati, prenotazione delle scorte, analitica
actionProductUpdateProdotto salvato in amministrazioneSincronizzazione di cataloghi esterni, reindicizzazione della ricerca, rigenerazione delle immagini
actionCustomerAccountAddSi registra un nuovo clienteEmail di benvenuto, sincronizzazione CRM, segmentazione
actionFrontControllerSetMediaIl front controller carica gli assetRegistrare 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()
        &amp;&amp; $this->registerHook('displayProductAdditionalInfo')
        &amp;&amp; $this->registerHook('actionCartSave')
        &amp;&amp; $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.

Pagina Posizioni del Back Office di PrestaShop 8 che mostra le registrazioni degli hook e l'ordinamento dei moduli

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 Cart non è quella che credi.
  • L'indice delle classi può degradarsi. cache/class_index.php memorizza 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:

SintomoCausa più probabileSoluzione
Il metodo viene chiamato ma non restituisce nullaPercorso del template errato, oppure return mancante su un hook di visualizzazioneAttiva il debug, controlla il percorso del template, conferma di aver restituito l'output renderizzato
Si attiva su ogni pagina mentre vuoi solo le pagine prodottoNessun controllo sul controllerif (!($this->context->controller instanceof ProductController)) return '';
Si attiva due volte sulla stessa paginaRegistrato due volte in ps_hook_moduleEsegui DELETE della riga duplicata, oppure reimposta il modulo
$params['product'] è un array e non un oggettoPS 1.7+ usa gli array del presenter nel front officeUsa $params['product']['id_product'], oppure chiama new Product($id) se ti serve l'oggetto completo
Funziona in dev, si rompe in prodTemplate Smarty compilati e memorizzati nella cacheForza 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

Caricamento...
Torna su