PrestaShop : un override qui n’est pas pris en compte, les cinq causes

Le fichier de surcharge est en place sur le serveur, il est lisible, et la boutique continue pourtant d’exécuter le code du cœur. Pas d’erreur 500, rien dans les logs, simplement l’ancien comportement. Je retrouve ce symptôme aussi bien sur des boutiques en 1.7 que sur des installations en 8.x et 9.x, et il vient presque toujours de l’une des cinq causes ci-dessous. Chaque section donne la vérification à faire, puis la correction à appliquer.

Le fichier class_index.php a-t-il été régénéré ?

Je commence toujours par regarder ce que l’autoloader a réellement enregistré, avant de toucher à la moindre ligne. PrestaShop ne parcourt pas le dossier override à chaque requête. Il s’appuie sur une carte des classes mise en cache dans var/cache/prod/class_index.php, ou dans var/cache/dev/class_index.php lorsque le mode debug est actif. La documentation développeur décrit ce fichier comme le lien entre une classe et son fichier de déclaration, et indique qu’il peut être supprimé sans risque. Tant que cette carte date d’avant la copie de ma surcharge, c’est la classe du cœur qui répond.

Le script ci-dessous se dépose à la racine de la boutique et affiche, pour les deux environnements, le chemin que l’index associe à une classe donnée.

<?php
/*
 * verif-index.php
 * A deposer a la racine de la boutique, au meme niveau que le dossier var/.
 * Execution en SSH :   php verif-index.php Product
 * A supprimer du serveur une fois le diagnostic termine.
 */

$classe = isset($argv[1]) ? $argv[1] : '';

if ($classe === '') {
    exit("Usage : php verif-index.php NomDeClasse\n");
}

foreach (array('prod', 'dev') as $env) {
    $index = __DIR__ . '/var/cache/' . $env . '/class_index.php';

    if (!is_file($index)) {
        echo '[' . $env . "] aucun index a cet emplacement\n";
        continue;
    }

    $carte = include $index;

    if (!isset($carte[$classe])) {
        echo '[' . $env . '] ' . $classe . " absente de l'index\n";
        continue;
    }

    /* Un chemin vide signifie qu'aucune surcharge n'est indexee pour cette classe. */
    $chemin = $carte[$classe]['path'];
    echo '[' . $env . '] ' . $classe . ' : '
        . (empty($chemin) ? 'aucune surcharge indexee' : $chemin) . "\n";
}

Si le script annonce qu’aucune surcharge n’est indexée alors que le fichier existe, la carte est périmée. Je sauvegarde le dossier override, puis je supprime l’index et je purge le cache Symfony.

# A executer depuis la racine de la boutique, en SSH.

# Sauvegarde du dossier override avant toute manipulation.
tar -czf ~/override-$(date +%F).tar.gz override/

# Suppression de l'index des classes : PrestaShop le reconstruit au chargement suivant.
rm -f var/cache/prod/class_index.php var/cache/dev/class_index.php

# Purge du cache Symfony. Adapter --env au mode debug reellement actif sur la boutique.
php bin/console cache:clear --env=prod

Le nom du fichier correspond-il exactement au nom de la classe ?

Deuxième vérification, la plus silencieuse. Pour construire sa carte des classes, PrestaShop lit chaque fichier PHP du dossier de surcharge et y cherche une déclaration de classe portant le nom du fichier, sans son extension. Un fichier Product.php qui déclarerait class MaSurchargeProduit n’est jamais indexé, et aucune erreur ne remonte. La casse compte également : sur un serveur Linux, product.php et Product.php sont deux fichiers distincts, ce qui explique les surcharges qui fonctionnent en local sous Windows et pas en production.

J’ouvre donc le fichier et je contrôle trois choses : le nom du fichier, le nom de la classe déclarée, et le parent, qui doit être la classe du cœur suffixée par Core. Voici une surcharge complète et fonctionnelle, valable de la 1.7 à la 9.x, qui conserve le comportement natif avant d’ajouter sa propre règle.

<?php
/*
 * Fichier : override/classes/Product.php
 *
 * Le nom du fichier, le nom de la classe et le parent doivent concorder :
 * Product.php contient class Product, qui etend ProductCore.
 */

class Product extends ProductCore
{
    /*
     * Un produit n'est plus presente comme nouveaute s'il est en rupture.
     * On delegue d'abord au coeur, puis on applique la regle de la boutique.
     */
    public function isNew()
    {
        if (!parent::isNew()) {
            return false;
        }

        $quantite = StockAvailable::getQuantityAvailableByProduct((int) $this->id);

        return $quantite > 0;
    }
}

Après correction du nom ou de l’héritage, je supprime à nouveau l’index des classes. Ce type de reprise fait partie des interventions que je mène en développement sur mesure sur des boutiques existantes.

Le fichier est-il au bon endroit dans le dossier override ?

L’arborescence du dossier override doit reproduire celle du cœur. Une surcharge de classe va dans override/classes/, une surcharge de contrôleur de boutique dans override/controllers/front/, une surcharge de contrôleur d’administration dans override/controllers/admin/. Ce n’est pas une convention décorative : sur les versions 1.7 et 8.0, l’autoloader ne balaie que override/classes/ et override/controllers/, et un fichier posé ailleurs n’entre jamais dans l’index. À partir de la 8.1 le balayage s’étend au dossier complet, mais l’installation d’une surcharge livrée par un module continue de s’appuyer sur le chemin miroir.

La boucle suivante compare chaque surcharge présente au fichier correspondant du cœur et signale celles qui n’ont pas de vis-à-vis.

# A executer depuis la racine de la boutique, en SSH. Lecture seule.

find override -type f -name '*.php' ! -name 'index.php' | while read -r fichier; do
  cible="${fichier#override/}"

  if [ -f "$cible" ]; then
    echo "OK          $fichier"
  else
    echo "A VERIFIER  $fichier  (aucun fichier du coeur en $cible)"
  fi
done

Toute ligne marquée « A VERIFIER » signale un chemin qui ne correspond à rien dans le cœur. Je déplace alors le fichier vers le chemin miroir exact, puis je régénère l’index des classes.

Le réglage « Désactiver toutes les surcharges » est-il actif ?

PrestaShop expose un interrupteur global qui neutralise les surcharges. Il se trouve dans le back-office, sous Paramètres avancés puis Performances, dans le bloc consacré au mode debug, et il est libellé « Désactiver toutes les surcharges ». Il est souvent basculé pendant une mise à jour ou un diagnostic, puis oublié. En coulisses, ce réglage correspond à la clé de configuration PS_DISABLE_OVERRIDES. Sur les versions 1.7 et 8.0, l’autoloader lit cette clé au moment de régénérer sa carte et reconstruit l’index sans le dossier de surcharge.

Quand le back-office est inaccessible, je lis la valeur directement en base. La requête de lecture ci-dessous est sans risque, celle de mise à jour modifie la configuration de la boutique : je sauvegarde la table avant de l’exécuter.

/* Prefixe de table par defaut : ps_. A adapter a votre installation. */

/* Lecture : la valeur doit etre 0 pour que les surcharges soient prises en compte. */
SELECT id_configuration, id_shop, name, value
FROM ps_configuration
WHERE name = 'PS_DISABLE_OVERRIDES';

/* Ecriture, apres sauvegarde de la table ps_configuration. */
UPDATE ps_configuration
SET value = '0', date_upd = NOW()
WHERE name = 'PS_DISABLE_OVERRIDES';

La bascule par le back-office déclenche elle-même la reconstruction de l’index. Après une modification en SQL, je supprime class_index.php à la main.

Une autre surcharge occupe-t-elle déjà la classe ?

Le dossier override est partagé par toute la boutique, et une seule définition par classe peut y vivre. Quand un module installe ses surcharges, PrestaShop insère au-dessus de chaque méthode copiée un commentaire portant son nom, sa version et la date. Si un second module tente de surcharger la même méthode, l’installation est refusée. Jusqu’à la 8.1, PrestaShop nomme le module responsable et sa version. Depuis la 8.2, un contrôle préalable signale le fichier en conflit. Le piège apparaît quand la première surcharge a été posée à la main, ou quand le module fautif a été désinstallé sans nettoyer son fichier.

Le script suivant relève ces marqueurs et les confronte à la table des modules, qui ne stocke que l’identifiant, le nom, la version et l’état actif.

<?php
/*
 * conflits-override.php
 * A deposer a la racine de la boutique, puis :   php conflits-override.php
 * Lecture seule : le script n'ecrit ni sur le disque ni en base.
 */

$racine = __DIR__;
$fichierParametres = $racine . '/app/config/parameters.php';

if (!is_file($fichierParametres)) {
    exit("app/config/parameters.php introuvable, adaptez le chemin.\n");
}

$parametres = include $fichierParametres;
$bdd = $parametres['parameters'];

$pdo = new PDO(
    'mysql:host=' . $bdd['database_host'] . ';dbname=' . $bdd['database_name'] . ';charset=utf8',
    $bdd['database_user'],
    $bdd['database_password']
);
$prefixe = $bdd['database_prefix'];

$modules = array();
foreach ($pdo->query('SELECT name, active FROM `' . $prefixe . 'module`') as $ligne) {
    $modules[$ligne['name']] = (int) $ligne['active'];
}

$fichiers = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator($racine . '/override', FilesystemIterator::SKIP_DOTS)
);

foreach ($fichiers as $fichier) {
    if ($fichier->getExtension() !== 'php' || $fichier->getFilename() === 'index.php') {
        continue;
    }

    $relatif = substr($fichier->getPathname(), strlen($racine) + 1);
    $contenu = file_get_contents($fichier->getPathname());

    /* Marqueur ecrit par PrestaShop au-dessus de chaque methode copiee par un module. */
    preg_match_all('/\*\s*module:\s*(\S+)/', $contenu, $trouves);
    $auteurs = array_values(array_unique($trouves[1]));

    if (count($auteurs) === 0) {
        echo $relatif . " : surcharge manuelle, aucun marqueur de module\n";
        continue;
    }

    foreach ($auteurs as $nom) {
        $etat = 'absent de la table module';

        if (array_key_exists($nom, $modules)) {
            $etat = $modules[$nom] === 1 ? 'installe et actif' : 'installe mais desactive';
        }

        echo $relatif . ' : ' . $nom . ' (' . $etat . ")\n";
    }

    if (count($auteurs) > 1) {
        echo "    plusieurs modules se partagent ce fichier\n";
    }
}

Quand deux modules se disputent la même méthode, je fusionne les deux logiques dans un seul fichier, ou je bascule l’un des deux sur un point d’accroche. Quand le besoin se limite à injecter du contenu dans un gabarit, un système de shortcodes maison rend le même service sans toucher au cœur. Ajouter un hook évite durablement ce genre de collision.

Ce qu’il faut vérifier après correction

Une fois la cause traitée, je contrôle que la surcharge est bien active et qu’elle n’a rien cassé au passage. Je relance le script de lecture de l’index pour confirmer que la classe pointe désormais vers le dossier de surcharge. Je charge ensuite une page de la boutique et une page du back-office, puis je consulte le journal d’erreurs PHP du serveur. C’est là que se voient les incompatibilités de signature : entre la 8.2 et la 9.0, par exemple, la méthode initContent du contrôleur produit a reçu un type de retour, et une surcharge écrite pour la 8.x provoque une erreur fatale sur la 9.x.

Je vérifie aussi le comportement boutique par boutique en multiboutique, je désinstalle puis réinstalle le module concerné pour valider son cycle de vie, et je retire du serveur les scripts de diagnostic. Si le blocage persiste après ces cinq vérifications, décrivez-moi le contexte avec la version exacte et le nom de la classe.

Questions fréquentes

Peut-on supprimer class_index.php sur une boutique en production ?

Oui. La documentation développeur indique que ce fichier fait le lien entre une classe et son fichier de déclaration et qu’il peut être supprimé sans risque. PrestaShop le reconstruit au chargement suivant. La seule conséquence est un temps de réponse un peu plus long sur la première requête, le temps que la carte soit régénérée.

Faut-il surcharger un contrôleur Symfony de l’administration ?

Non. Le dossier de surcharge ne concerne que les classes et contrôleurs historiques. Pour les pages déjà migrées, la documentation officielle oriente vers la décoration du service ou le remappage de la route, et présente la décoration comme la méthode à privilégier. Une surcharge posée dans le dossier override reste sans effet sur ces pages.

Comment surcharger la classe principale d’un autre module ?

Le mécanisme est différent de celui des classes du cœur. Le fichier se place dans override/modules/nomdumodule/nomdumodule.php et déclare une classe portant le nom du module suivi du suffixe Override, qui étend la classe du module. PrestaShop charge ce fichier au moment d’instancier le module, sans passer par la carte des classes. Quand les adaptations deviennent trop lourdes, dupliquer le module sous un autre nom évite de subir chaque mise à jour de l’original.

Pourquoi ma surcharge de contrôleur d’administration ne s’exécute-t-elle plus en 9.x ?

Plusieurs méthodes du flux historique ne sont plus appelées directement dans la version 9. La documentation cite notamment run, initHeader, initContent, initFooter et display sur la classe AdminController. Le fichier de surcharge est bien chargé, mais la méthode redéfinie n’est plus invoquée par le nouveau flux.

Commentaires

Un projet ? Parlons-en.

Devis gratuit et sans engagement. Je réponds dans les douze heures.