«Cannot use object of type Product as array»: le schede prodotto vanno in 500 a intermittenza
Il sintomo: clienti veri che vedono una pagina di errore, a intermittenza
Un negozio PrestaShop 1.7 con un tema su misura registrava una settantina di errori 500 al giorno, circa uno ogni 15 minuti, tutti su schede prodotto e sulle richieste di aggiunta al carrello. Non bot: indirizzi italiani, browser normali (Safari, Chrome). Ogni errore di questi è un cliente vero che stava guardando un prodotto, o stava per comprarlo, e si è trovato davanti una pagina bianca.
Il log riportava sempre lo stesso errore fatale:
PHP Fatal error: Uncaught Error:
Cannot use object of type Product as array
in .../product-flags.tpl.php
on line 44
La riga incriminata, nel file compilato da Smarty, corrispondeva a $product.flags nel template sorgente product-flags.tpl, il partial che disegna le etichette "in saldo", "pacco", "nuovo" sotto il nome del prodotto. Non capitava su tutte le schede prodotto, e non sempre sugli stessi prodotti.
Cosa sembrava e non era
Prima di arrivare alla causa, tre sospetti ragionevoli sono stati controllati e scartati uno per uno.
- Un modulo agganciato agli hook della scheda prodotto. Il sospetto naturale, con un tema su misura, è che qualche modulo di terze parti stia sovrascrivendo la variabile
productpassata a Smarty. Sono stati controllati tutti i moduli agganciati agli hook rilevanti (chat, recensioni, blocchi correlati, tracciamento) uno per uno: nessuno assegnavaproductoproductsin un modo che potesse spiegare l'errore. - L'override personalizzato del controller prodotto. Il negozio aveva un override del controller di scheda prodotto, scritto anni prima. Controllato riga per riga: tocca solo l'assegnazione della categoria, non la variabile che genera l'errore.
- "Sono prodotti con dati corrotti nel database." I prodotti coinvolti, controllati singolarmente, erano dati normali: niente di rotto nei campi, nessun valore mancante. Il tratto in comune, quando si guardavano tutti insieme, era avere almeno un'etichetta attiva (promozione, pacco), non un dato malformato.
Nessuna delle tre piste spiegava perché l'errore fosse intermittente: capitava a volte su un prodotto e non su un altro identico per struttura, e sullo stesso prodotto a volte si è presentato e a volte no.
La causa vera (parziale, e va detto con onestà)
In PrestaShop, la variabile $product che arriva ai template normalmente è un ProductLazyArray: una classe che implementa ArrayAccess, quindi $product['flags'] in PHP e $product.flags in Smarty funzionano perché l'oggetto sa comportarsi come un array anche se non lo è davvero. In certi contesti però arriva un oggetto Product "grezzo", cioè l'ObjectModel vero e proprio, che non implementa ArrayAccess. Accedere a un campo di un oggetto simile con la sintassi da array non genera un avviso recuperabile: genera un Error fatale, non catturabile da PrestaShop, che interrompe l'esecuzione e restituisce un 500 al visitatore.
Va detto con la stessa onestà con cui è stato affrontato sul negozio vero: chi, esattamente, passa l'oggetto Product grezzo al posto del ProductLazyArray non è stato individuato nella finestra di intervento disponibile. Sono stati esclusi gli hook più ovvi, ma il punto preciso di iniezione resta da isolare, probabilmente in un punto meno visibile del codice del tema o in un contesto particolare del ciclo di rendering. Questo è anche il motivo per cui l'intervento che segue è stato applicato consapevolmente come mitigazione, non come correzione della causa radice.
La correzione: rendere il template tollerante, non indovinare chi sbaglia
Quando la causa esatta non è isolabile nei tempi a disposizione, e l'effetto è un sito che risponde 500 ai clienti, la priorità cambia: prima si toglie l'urgenza (il 500 sparisce), poi, con calma, si continua a cercare chi inietta il dato sbagliato. Il template è stato modificato per leggere le etichette solo quando $product è davvero accessibile come un array, e per restare silenzioso (etichette vuote, non pagina rotta) quando non lo è:
{assign var='flags'
value=null}
{if isset($product) &&
($product|is_array ||
$product|is_a:
'ArrayAccess')}
{assign var='flags'
value=$product.flags}
{/if}
<ul class="product-flags">
{if $flags}
{foreach from=$flags
item=flag}
...
{/foreach}
{/if}
</ul>
Copre entrambi i casi legittimi: un array PHP semplice (is_array) e un oggetto tipo ProductLazyArray (is_a:'ArrayAccess'). Quando arriva l'oggetto grezzo che non è né l'uno né l'altro, il blocco resta semplicemente vuoto, invece di far fallire tutta la pagina.
Un controllo da fare prima, non dopo. Usare un modificatore Smarty come |is_array o |is_a presuppone che il tema permetta di chiamare funzioni PHP dai template. Su molte installazioni questo è vero di default, ma non su tutte: se esiste una policy di sicurezza Smarty che lo vieta, lo stesso identico cambiamento romperebbe la compilazione di ogni scheda prodotto, non solo il caso che si voleva sistemare. Prima di scrivere il modificatore, vale la pena controllare se il tema ne usa già uno equivalente da qualche parte (in questo caso, |count compariva già diciassette volte nello stesso tema): se c'è già, la strada è aperta.
Come verificare che tenga
- Pulire solo il compilato del template coinvolto, trovato con
finddentrovar/cache/prod/smarty/compile/, mai la cartella intera della cache: è l'unico modo di forzare la ricompilazione di quel singolo file senza pagare il costo di ricostruire tutta la cache Smarty del negozio. - Ricaricare le pagine dei prodotti che prima fallivano e controllare due cose insieme: che rispondano 200, e che le etichette (promozione, sconto) siano ancora visibili dove ci si aspetta che ci siano. Un template reso "tollerante" in modo sbagliato può smettere di generare errori nascondendo anche i dati buoni, non solo quelli rotti.
- Contare le occorrenze dell'errore nel log prima e dopo, non fermarsi al primo controllo:
grep -c "Cannot use object of type Product as array" error.loga distanza di qualche ora, non di qualche minuto, perché l'errore si presentava a intervalli di decine di minuti, e un test troppo breve può sembrare un successo per puro caso. - Tenere un promemoria per la causa radice. La mitigazione toglie l'urgenza, non chiude il caso: vale la pena controllare, in un momento successivo, se lo stesso schema (
$qualcosa.chiavesu un partial che si aspetta un array ma a volte riceve un oggetto) si ripete altrove nello stesso tema, perché un solo punto di iniezione difettoso può benissimo toccare più di un template.
Il negozio resta anonimo di proposito: il meccanismo è quello che conta. Se in un tema PrestaShop compare Cannot use object of type Product as array su un partial che legge $product.qualcosa, il sospetto giusto non è "quel prodotto è rotto": è che in quel punto specifico del rendering, a volte, arriva un Product vero invece del ProductLazyArray che il template si aspetta.
