Surcharge du calculateur de total panier dans Prestashop 1.7

Problématique

On veut intervenir dans le calcul du total panier.

Cette action se répartit dans plusieurs classes. Ça part de la classe Cart dédiée dans la fonction getOrderTotal(). Celle-ci contient uniquement la logique générale, ce n’est pas directement elle qui fait les calculs. Elle utilise un objet de la classe Calculator, lui même retourné par la fonction newCalculator() de la classe Cart, puis fait appel à différentes fonctions selon le calcul voulu. On souhaite intervenir dans les calculs, c’est alors la classe Calculator qu’il faut modifier.

Solution

On va donc créer une classe héritée de Calculator où on va modifier les fonctions de calcul.

Problème : la classe Calculator fait partie du dossier src et n’est pas appelée comme un service Symfony, on ne peut pas la surcharger avec le système des overrides de Prestashop.

On ne peut surcharger que la classe Cart. On va donc viser la fonction newCalculator() et envisager de remplacer l’instanciation de l’objet en utilisant une classe héritée de Calculator, qu’on appellera MyCalculator.

Problème : cette instanciation, c’est la première ligne de la fonction newCalculator(). Elle fait ensuite d’autres traitements qui modifient l’objet avant de le retourner. Il faudrait donc copier/coller tout le reste du code de la fonction. Autrement dit on surcharge par remplacement au lieu de surcharger par extension. Et c’est pas top, ça voudra dire qu’il faudra surveiller dans les mises à jour de Prestashop si cette fonction a changé et répercuter les changements le cas échéant.

Surcharger par extension implique de s’assurer que la fonction originale s’exécute et donc de commencer notre surcharge par un parent::newCalculator(…) et sans copier/coller le reste de la fonction.

Sauf que là on a un objet de la classe Calculator alors qu’on veut retourner un MyCalculator sur lequel aurait été réalisé les mêmes traitements que sur l’objet original. Concrètement on voudrait caster l’objet dans une classe héritée de celle à laquelle il appartient. Caster, ce n’est pas possible. Par contre on peut instancier un objet MyCalculator et copier les propriétés depuis l’objet de classe Calculator.

Problème : les propriétés de la classe Calculator sont toutes protégées. On ne peut pas y accéder depuis un objet, seulement depuis une classe héritée. Et du coup les propriétés de la classe héritée sont aussi protégées, on ne peut pas non plus leur donner de valeur directement depuis l’objet.

Il faut agir directement dans le constructeur de la classe héritée. Et pour récupérer les valeurs de propriété on va utiliser la classe de réflexion ReflectionClass. Celle-ci permet d’obtenir tout un tas d’informations sur une classe sur la base du nom de la classe ou d’un objet. Et surtout, elle est capable de rendre accessibles les valeurs des propriétés protégées et privées d’un objet.

Cette solution convient car la classe MyCalculator n’a pas vocation à être utilisée en dehors de ce contexte et ses objets seront toujours construits à partir d’objets de la classe parente Calculator.

Cela nécessitera de surcharger toute fonction qui instancierait un objet de la classe Calculator. Spoiler: actuellement, cela n’est fait que dans la classe Cart, à un seul endroit.

Résultat

use PrestaShop\PrestaShop\Core\Cart\Calculator;

class MyCalculator extends Calculator
{
    public function __construct($calculator)
    {
        $reflectedSourceObject           = new \ReflectionClass($calculator);
        $reflectedSourceObjectProperties = $reflectedSourceObject->getProperties();

        foreach ($reflectedSourceObjectProperties as $reflectedSourceObjectProperty) {
            $propertyName = $reflectedSourceObjectProperty->getName();
            $reflectedSourceObjectProperty->setAccessible(true);
            $this->$propertyName = $reflectedSourceObjectProperty->getValue($calculator);
        }
    }
}

class Cart extends CartCore
{
    public function newCalculator($products, $cartRules, $id_carrier)
    {
        $calculator = parent::newCalculator($products, $cartRules, $id_carrier);

        return new MyCalculator($calculator);
    }
}

Ainsi on retourne bien à getOrderTotal() un objet qui est une copie conforme de l’objet original mais d’une classe héritée sur laquelle on a complètement la main.