Prestashop : Hhmodule manager, fonctionnement technique et extension

Dans ma série d’articles précédents sur la mise en place de la CI/CD sur Prestashop, j’ai présenté une étape qui permets d’automatiser le déploiement de changements de modules et de configuration.
Celle-ci est géré via le module hhmodulesmanager, dont j’ai également présenté le fonctionnement basique qui est disponible dans cet article : Prestashop : Comment limiter les interactions manuelles avec le déploiement continu
Ce module gère nativement les actions suivantes :

  • Installation / Désinstallation / Activation / Désactivation / Mise à jour de module
  • Ajout / Mise à jour / Suppression de configuration

En revanche avec sa conception il peut servir de base technique pour gérer pleins d’autres actions, telles que  la Création/Modification/Suppression d’entités spécifiques…
Nous verrons plus loin comment le mettre en place.
Tout d’abord voici le fonctionnement technique du module et les notions importantes.
Pour rappel pour vous télécharger le module (gratuitement) sur la boutique ici :
Pensez à vous abonnez aux mises à jour de celui-ci pour être informés de la sortie des dernières versions 🙂

Télécharger le module complet ( et gratuit ) sur la boutique

Objet « Change »

Chaque action effectuée dans la back office est retranscrite sous la forme d’un objet « Changement » qui est une instance de la classe Change.
Cette classe est visible dans le dossier src du module.

Cet objet à les propriétés suivantes :

  • id : Identifiant technique du changement
  • entity : code de l’entité qui a eut le changement ( nativement : configuration / module )
  • key : clé qui permets de récupérer l’entité qui a bougé ( ex: nom de la clé de configuration / nom du module )
  • details : Données du changement encodées en json

Dans le module natif, ces changements sont créés automatiquement lorsqu’une action est effectuée sur un module ou une configuration.
Via des hooks spécifiques.
Dans le cas d’une personnalisation il faudra identifier à quel moment générer ce changement.
L’ensemble de ces changements sont visibles et listés dans l’administration.

Converters

Dans le controller de l’administration qui permets de visualiser les changements, lorsqu’on va demander la création d’un nouveau fichier d’upgrade.

(Le code de la partie suivante est gérée dans le fichier src/Patch/Generator.php )
Pour chaque changement on va récupérer la liste des « Converters ».
Ces classes sont chargées de convertir un changement ( ie objet Change ), sous la forme d’un tableau représentatif de ce changement, qui sera ensuite formaté dans le yml récapitulant les mises à jours.
Toutes les classes « converters » sont déclarées via le fichier config/services.yml, et doivent implémenter l’interface \Hhennes\ModulesManager\Converter\ConverterInterface.
Plusieurs converters différents peuvent traiter un même changement si nécessaire.
Nous verrons plus bas un exemple concret de sa mise en œuvre.

Upgraders

A l’inverse, lors de l’exécution de la mise à jour des données, via la commande console : php bin/console hhennes:module-manager:manage -v
Il est nécessaire de retraduire les mises à jour décrites dans les fichiers yml, en actions à réaliser pour les appliquer.
C’est ici qu’on va aller chercher les classes « Upgraders », qui sont également déclarées via le fichier config/services.yml, et qui doivent implémenter l’interface \Hhennes\ModulesManager\Upgrader\UpgraderInterface

Étendre les fonctionnalités du module dans votre propre module.

Maintenant que les concepts basiques sont présentés, et ( plus ou moins ) compris, on va passer à un cas pratique pour l’ilustrer.
Pour l’exemple on souhaite pouvoir gérer dans les mises à jour automatique les actions suivantes d’un objet « Sample » qui étends l’ObjetModel standard de Prestashop.
Le module Sample peut être télécharger ici :  hhsamplemodule ( tiré d’un ancien tutoriel sur les grilles admin et l’objectmodel)

Pour commencer on va créer un nouveau module  hhmodulesmanageraddon , avec le code de base suivant :
Le module utilisera composer via le namespace Hhennes\ModulesManagerAddon
Le code du module de base sers à répondre à la première problématique qui est de générer un changement.

use \Hhennes\ModulesManager\Change;
 
class Hhmodulesmanageraddon extends Module
{
 
    public function __construct()
    {
        $this->name = 'hhmodulesmanageraddon';
        $this->tab = 'others';
        $this->version = '0.1.0';
        $this->author = 'hhennes';
        $this->dependencies = ['hhmodulesmanager'];
        $this->bootstrap = true;
        parent::__construct();
 
        $this->displayName = $this->l('Module Manager addon');
        $this->description = $this->l('Module manager addon');
 
    }
 
    public function install()
    {
        return parent::install()
            && $this->registerHook(
                [
                    'actionHhmodulesmanagerExcludeConfiguration', //Hook spécifique pour ignorer des conf
                    'actionObjectSampleAddAfter', //Ajout d'un nouvel objet "Sample"
                    'actionObjectSampleUpdateAfter',//Mise à jour de l'objet "Sample"
                    'actionObjectSampleDeleteAfter', //Suppression de l'objet "Sample"
                ]
            );
    }
 
    /**
     * Ajout de configurations ignorées dans le module HHmodulesmanager
     *
     * @param array $params
     * @return void
     */
    public function hookActionHhmodulesmanagerExcludeConfiguration(array $params): void
    {
        $params['configuration'][]= 'PS_CCCJS_VERSION';
        $params['configuration'][]= 'PS_CCCCSS_VERSION';
    }
 
    /**
     * Après la création d'un objet sample
     *
     * @param array $params
     * @return void
     */
    public function hookActionObjectSampleAddAfter(array $params): void
    {
        try {
            $sampleObject = $params['object']; //Récupération de l'objet
            $change = new Change(); //Création d'un changement
            $change->entity = 'Sample'; //Entité
            $change->action = 'add'; //Action
            $change->key = $sampleObject->code; //Code de l'objet
            $change->details = json_encode($sampleObject); //Détails de l'objet
            $change->add();
        } catch (\PrestaShopException $e) {
            PrestaShopLogger::addLog(
                'Unable to register change in ' . __METHOD__,
                '0',
                1,
            );
        }
    }
 
    /**
     * Après la mise à jour d'un objet sample
     *
     * @param array $params
     * @return void
     */
    public function hookActionObjectSampleUpdateAfter(array $params): void
    {
        try {
            $sampleObject = $params['object'];
            $change = new Change();
            $change->entity = 'Sample';
            $change->action = 'update';
            $change->key = $sampleObject->code;
            $change->details = json_encode($sampleObject);
            $change->add();
        } catch (\PrestaShopException $e) {
            PrestaShopLogger::addLog(
                'Unable to register change in ' . __METHOD__,
                '0',
                1,
            );
        }
    }
 
    /**
     * Après la mise à jour d'un objet sample
     *
     * @param array $params
     * @return void
     */
    public function hookActionObjectSampleDeleteAfter(array $params): void
    {
        try {
            $sampleObject = $params['object'];
            $change = new Change();
            $change->entity = 'Sample';
            $change->action = 'delete';
            $change->key = $sampleObject->code;
            $change->details = 'Content removed';
            $change->add();
        } catch (\PrestaShopException $e) {
            dump($e->getMessage());
            PrestaShopLogger::addLog(
                'Unable to register change in ' . __METHOD__,
                '0',
                1,
            );
        }
    }
}

Pour vérifier que cela fonctionne, créer, modifier, supprimer des entités « Sample » et des nouveaux changements doivent remonter.

Prochain élément à faire générer le converter, pour convertir notre changement en action dans le yml.
Pour commencer il faut déclarer le nouveau converter au module hhmodulesmanager via le fichier config/services.yml , en y ajoutant le code suivant :

services:
  _defaults:
    public: true
 
  #Add a custom converter
  hhennes.modulesmanageraddon.converter.sampleobject: #Nom du service
    class: Hhennes\ModulesManagerAddon\Converter\SampleObject #Classe de l'objet
    tags: [ hhennes.modulesmanager.converter ] #Tag à implémenter pour enregistrer le converter

Le code de la classe du converter sera ensuite le suivant :

<?php
 
namespace Hhennes\ModulesManagerAddon\Converter;
 
use Hhennes\ModulesManager\Change;
use Hhennes\ModulesManager\Converter\ConverterInterface;
 
class SampleObject implements ConverterInterface
{
    public const TYPE = 'Sample';
 
    /**
     * @var array Allowed actions
     */
    public const ALLOWED_ACTIONS = [
        'add',
        'update',
        'delete'
    ];
 
    /**
     * @inheritDoc
     */
    public function canConvert(Change $change): bool
    {
        return $change->entity === self::TYPE; //Si l'entité du changement est = à "Sample" , on peut le convertir
    }
 
    /**
     * @inheritDoc
     * Notez bien que le tableau des changements est passé par référence, ce qui permets de travailler sur le même tableau 
     * Pour tous les converters, peut importe leur classe. 
     */
    public function convert(Change $change, array &$currentChangesArray): void
    {
        //On vérifie que l'action du changement est gérée, sinon on renvoie une erreur
        if (!in_array($change->action, self::ALLOWED_ACTIONS)) {
            throw new Exception('Unknow ' . self::TYPE . ' action , allowed values : ' .
                implode(',', self::ALLOWED_ACTIONS));
        }
        //Création d'une entrée "Sample" qui correspond à notre objet dans le tableau, si elle n'existe pas encore
        if (!array_key_exists(self::TYPE, $currentChangesArray)) {
            $currentChangesArray[self::TYPE] = [];
        }
        //Création dans ce tableau de l'action à effectuer sur cette entité ( add/update/delete)
        if (!array_key_exists($change->action, $currentChangesArray[self::TYPE])) {
            $currentChangesArray[self::TYPE][$change->action] = [];
        }
        //Création du détail de l'entité concernée sous la forme : code => valeur
        if (!in_array($change->key, $currentChangesArray[self::TYPE][$change->action])) {
            $currentChangesArray[self::TYPE][$change->action][$change->key] = $change->details;
        }
    }
}

Pour vérifier que cela fonctionne, générer une mise à jour concernant des objets samples depuis la liste des changements dans le back office.
Si tout est bon on pourrait avoir les informations suivantes, qui correspondent bien à l’ensemble de nos cas.

Sample:
    add:
        new_ci: '{"id":"5","name":"NouveauCI","code":"autre","email":"[email protected]","title":{"1":"hrve","2":"ffdfddf"},"description":{"1":"fdsfshjhjf","2":"dsdsdds"},"id_shop_list":[],"force_id":false}'
        new_ci_1: '{"id":"5","name":"NouveauCIBIS","code":"autre","email":"[email protected]","title":{"1":"hrve","2":"ffdfddf"},"description":{"1":"fdsfshjhjf","2":"dsdsdds"},"id_shop_list":[],"force_id":false}'
    update:
        new_ci_2: '{"id":"5","name":"NouveauCITER","code":"autre","email":"[email protected]","title":{"1":"hrve","2":"ffdfddf"},"description":{"1":"fdsfshjhjf","2":"dsdsdds"},"id_shop_list":[],"force_id":false}'
    delete:
        new_ci_3: 'Content removed'

C’est donc parti pour la dernière partie qui va être d’écrire notre upgrader.
Pour commencer il faut également le déclarer dans le fichier config/services.yml

#Add a custom upgrader
  hhennes.modulesmanageraddon.upgrader.sampleobject:
    class: Hhennes\ModulesManagerAddon\Upgrader\SampleObject
    tags: [ hhennes.modulesmanager.upgrader ] #Tag à implémenter pour enregistrer l'upgrader

Puis ensuite créer l’upgrader correspondant.

namespace Hhennes\ModulesManagerAddon\Upgrader;
 
//On charge le fichier de la classe
require_once _PS_MODULE_DIR_ . '/hhsamplemodule/classes/Sample.php';
 
use Hhennes\ModulesManager\Upgrader\UpgraderInterface;
use Hhennes\ModulesManager\Upgrader\UpgraderResultTrait;
use Sample;
 
class SampleObject implements UpgraderInterface
{
    /**
     * Use a trait to avoid to define repetitives methods
     */
    use UpgraderResultTrait;
 
    /** @var string Type d'upgrade */
    public const TYPE = 'Sample';
 
    public function upgrade(array $data): void
    {
        //Les données du yml sont récupérées sous forme de tableau.
        //Si pas de clé correspondante à notre entité on ne fait rien.
        if (!array_key_exists(self::TYPE, $data)) {
            return;
        }
        $data = $data[self::TYPE];
 
        //Ajouts
        if (array_key_exists('add', $data)) {
            foreach ($data['add'] as $entityCode => $entityData) {
                try {
                    $entityArray = json_decode($entityData, true); //L'objet est stocké en json, on le transforme en array pour récupérer les infos.
                    $sample = new Sample();
                    $sample->code = $entityCode;
                    $sample->name = $entityArray['name'];
                    $sample->email = $entityArray['email'];
                    $sample->title = $entityArray['title'];
                    $sample->description = $entityArray['description'];
                    $sample->save();
                    $this->success[] = 'create sample entity code ' . $entityCode; //Message de succès pour les logs
                } catch (\Throwable $e) {
                    $this->errors[] = 'Unable to create sample entity code ' . $entityCode . ' Error ' . $e->getMessage();
                }
            }
        }
        //Mises à jours
        if (array_key_exists('update', $data)) {
            foreach ($data['update'] as $entityCode => $entityData) {
                try {
                    $entityArray = json_decode($entityData, true);
                    $idSample = Sample::getIdByCode($entityCode);
                    if ($idSample > 0) {
                        $sample = new Sample($idSample);
                        $sample->code = $entityCode;
                        $sample->name = $entityArray['name'];
                        $sample->email = $entityArray['email'];
                        $sample->title = $entityArray['title'];
                        $sample->description = $entityArray['description'];
                        $sample->save();
                        $this->success[] = 'Update sample entity code ' . $entityCode;
                    }
                } catch (\Throwable $e) {
                    $this->errors[] = 'Unable to update sample entity code ' . $entityCode . ' Error ' . $e->getMessage();
                }
            }
        }
 
        //Suppressions
        if (array_key_exists('delete', $data)) {
            foreach ($data['delete'] as $entityCode => $entityData) {
                try {
                    $idSample = Sample::getIdByCode($entityCode);
                    if ($idSample > 0) {
                        $sample = new Sample($idSample);
                        $sample->delete();
                        $this->success[] = 'Delete sample entity code ' . $entityCode;
                    }
                } catch (\Throwable $e) {
                    $this->errors[] = 'Unable to update sample entity code ' . $entityCode . ' Error ' . $e->getMessage();
                }
            }
        }
    }
}

On a à présent tout terminé, plus qu’à déployer notre mise à jour et à voir si elle est bien prise en compte !
Comme on peut voir ci-dessous, tout se passe comme prévu, et les changements sur nos entités sont bien en place également 🙂

Un point important à noter est que pour aller plus vite :  le seul élément qui est réellement obligatoire est de définir un upgrader.
Vous pouvez tout à fait saisir directement vos changements à la main dans un fichier yml

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *