Archives de l’auteur : Numa

Cigales

16 Juin 2023
Vézénobres : Première écoute de l’année, ma maman me dit qu’elle les a entendus depuis quelques jours déjà (ce qui me semble tôt, pourtant les chaleurs ne sont pas trop fortes pour l’instant)

28 Juin 2024
Premiers chant de l’année (Vézénobres)

Blog : Découverte et mémoire

Je vais à présent utiliser cette section comme mémoire perso (que je découvre, que j’observe, dont je veux me souvenir etc..)

L’idée initiale était de me développer un outil perso stockant toutes mes notes, avec une fonctionnalité « il y’a un an, deux ans, trois ans, etc ». je n’ai pas le temps de m’en occuper pour l’instant mais je vais remplir des notes via ce blog pour une potentielle utilisation future.

Je place dans une catégorie « Temps qui passe » pour pouvoir extraire facilement. Pour l’instant aussi dans la catégorie « Partage » (que j’utilise finalement très peu et probablement plus) simplement pour que ce soit à part du blog technique. C’est donc disponible publiquement, bien que personne ne le sache…

Je commence cette pratique le 23 juin 2023 mais je publierais sans doute des événements dans le passé pour placer quelques notes déjà prise ailleurs.

Mon premier script Python

Pour marquer la date d’une pierre blanche !

Après plus de 15 ans de bons et loyaux service en PHP (et ce n’est pas terminé), j’ai l’occasion de travailler sur mon premier projet Python, et en ce mercredi 15 Février 2023 j’ai développé mon premier script dans ce langage.

Alors oui c’est basique mais je le pose ici pour le souvenir

import psycopg2
import csv

from psycopg2 import Error
#config :
# id de la liste que l'on veux remplir
id_liste = 300
# nom du fichier csv
filename = 'liste300.csv'
#
try:
    #connexion à la base
    conn = psycopg2.connect(database="nom_de_la_base",
                        host="localhost",
                        user="user_base",
                        password="pass_base",
                        port="5432")

    cursor = conn.cursor()
    #on vidange la liste au cas ou elle ne soit pas vierge
    cursor.execute("DELETE FROM taxonomie.cor_nom_liste WHERE id_liste = %s", (id_liste,))

    #parcours CSV
    nbInsert = 0
    nbError = 0;
    with open(filename, 'r') as csvfile:
        datareader = csv.reader(csvfile)
        for row in datareader:
            if row[0] != "cd_nom":
                #on recherche le cd_nom dans la table bib_nom pour récupérer la valeur id_nom
                cd_nom = row[0]
                cursor.execute("SELECT * FROM taxonomie.bib_noms WHERE cd_nom = %s", (cd_nom,))
                #on indique si la valeur n'existe pas
                nom = cursor.fetchone()
                if nom is None:
                    #raise Exception('cd_nom "' + str(cd_nom) + '" introuvable')
                    print('cd_nom "' + str(cd_nom) + '" introuvable ('+row[16]+') ')
                    nbError += 1
                else:
                    #insérer les données
                    id_nom = nom[0]
                    cursor.execute("INSERT INTO taxonomie.cor_nom_liste (id_nom, id_liste) VALUES(%s, %s)", (id_nom, id_liste,))
                    nbInsert += 1

    #commit
    conn.commit()
    print(str(nbInsert)+" lignes insérées")
    print(str(nbError)+" lignes ignorées")

except (Exception, Error) as error:
    print("Le script d'import retourne l'erreur : ", error)
finally:
    if conn:
        cursor.close()
        conn.close()

J’en profite pour remercier mon partenaire Iulian de me sortir de ma zone de confort, et qui après le NodeJs me lance sur Python. Je le remercie aussi de me permettre de me régaler sur d’autre projets avec l’utilisation de Symfony mon framework adoré !

Staphylin noir : Rencontre avec ce long insecte noir

J’ai croisé cet alien ce 4 Janvier dans la maison.
Je me suis bien demandé de quoi il s’agissait, je n’avais pas souvenir d’en avoir déjà vu.
Une simple recherche web « long insecte noir » à suffit pour m’apporter la réponse, il s’agit du Staphylin noir.

Voir wikipedia, d’où j’ai récupéré la photo d’illustration qui correspnd totalement à ce que j’ai vu.
https://fr.wikipedia.org/wiki/Ocypus_olens

Staphylin noir

Symfony 6.1 : Corriger la dépréciation « The « Symfony\Component\Console\Command\Command::$defaultName » property is considered final…

Depuis Symfony 6.1, si vous avez des commandes sous la forme suivante :

class MaCommandeCustomCommand extends Command {

    protected static $defaultName = 'app:ma-commande-custom';
    ...
}

Vous allez remarquer les dépréciations suivantes :
– The « Symfony\Component\Console\Command\Command::$defaultName » property is considered final. You should not override it in « … ».
– Since symfony/console 6.1: Relying on the static property « $defaultName » for setting a command name is deprecated. Add the « Symfony\Component\Console\Attribute\AsCommand » attribute to the « … » class instead.

Il suffit de passer sous le format suivant pour corriger ces erreurs :

use Symfony\Component\Console\Attribute\AsCommand;
...

#[AsCommand(
   name: 'app:ma-commande-custom',
)]
class MaCommandeCustomCommand extends Command {
    ...
}

Symfony 5 : Limiter la possibilité de « switch_user » en fonction du rôle de l’utilisateur cible.

Il existe dans Symfony une fonctionnalité permettant à certains utilisateurs de « se connecter en tant que » n’importe quel autre utilisateur (Documentation). Cette fonctionnalité est très utile pour que les administrateurs puissent se connecter sur le compte d’un client et vérifier les éventuels dysfonctionnements qui le concerne.

Cependant, dans son fonctionnement par défaut, dés que l’on attribut le rôle « ROLE_ALLOWED_TO_SWITCH » à un utilisateur il peux se connecter sur tout les autres utilisateurs existants. Cela peux s’avérer critique pour la sécurité de l’application, l’utilisateur pouvant alors gagner tous les privilèges. Concrètement si l’on prends un cas assez simple d’une application avec 3 rôles utilisateurs principaux : « ROLE_USER », « ROLE_ADMIN », « ROLE_SUPERADMIN » et que l’on définis la hiérarchie comme ceci :

security:
   # ...
    firewalls
:
        main
:
           # on active la fonctionnalité "switch_user"
            switch_user
: true  
    #
    role_hierarchy
:
        ROLE_ADMIN
:      [ROLE_USER, ROLE_ALLOWED_TO_SWITCH]
        ROLE_SUPER_ADMIN
: ROLE_ADMIN

Cela signifie que les administrateur et les superadmin pourront se connecter sur n’importe quel utilisateur. Mais cela implique que les simples administrateurs pourront devenir « superadmin » et donc avoir des droits auxquels in n’ont normalement pas accès. Nous allons voir comment limiter cela.

L’objectif est le suivant : les administrateurs « ROLE_ADMIN » peuvent se connecter sur tous les autres utilisateurs (user et administrateur) mais pas sur les superadmin. Pour les superadmin rien ne change, ils ont tous les accès.
La documentation de symfony explique la marche à suivre dans sa section limiting user switching. En suivant ce modèle on va donc modifier notre configuration sur 2 points : la propriété « switch_user » à qui on affecte un attribut « CAN_SWITCH_USER » et on supprime de notre hiérarchie le « ROLE_ALLOWED_TO_SWITCH » qui n’a plus d’utilité :

security:
   # ...
    firewalls
:
        main
:
            switch_user
: { role: CAN_SWITCH_USER }
    #
    role_hierarchy
:
        ROLE_ADMIN
:      ROLE_USER
        ROLE_SUPER_ADMIN
: ROLE_ADMIN

Pour la suite, on se base toujours sur la documentation pour créer le « Voter » correspondant, mais on se trouve confronté à un problème. On veux connaitre le rôle de l’utilisateur cible pour limiter l’accès si il est superadmin. Pour faire les choses proprement avec une hiérarchie des rôles réellement prise en charge on va utiliser notre service personnalisé « Securizer » permettant d’utiliser la méthode « isGranted » sur n’importe quel utilisateur. Des précisions sur l’utilité de ce service sont expliqué dans l’article précédent. Mais pour faire simple il suffit de créer le service suivant :

namespace App\Service;

use App\Entity\User;//l'entité user de notre aplication
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;

class Securizer {

    private $accessDecisionManager;

    public function __construct(AccessDecisionManagerInterface $accessDecisionManager) {
        $this->accessDecisionManager = $accessDecisionManager;
    }

    public function isGranted(User $user, $attribute, $object = null) {
        $token = new UsernamePasswordToken($user, 'none', 'none', $user->getRoles());
        return ($this->accessDecisionManager->decide($token, [$attribute], $object));
    }

}

et de l’utiliser das notre Voter pour l’attribut « CAN_SWITCH_USER » qui est maintenant facile à mettre en place en adaptant l’exemple de la documentation, le code complet est le suivant :

<?php
namespace App\Security\Voter;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;
use App\Service\Securizer;
use App\Entity\User;

class SwitchUserVoter extends Voter {

    private $security;
    private $securizer;

    public function __construct(Security $security, Securizer $securizer) {
        $this->security = $security;
        $this->securizer = $securizer;
    }

    protected function supports($attribute, $subject) {
        return in_array($attribute, ['CAN_SWITCH_USER']) && $subject instanceof User;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token) {
        $user = $token->getUser();
        //l'utilisateur doit être connecté et la cible doit être un utilisateur
        if (!$user instanceof User || !$subject instanceof User) {
            return false;
        }
        //on ne peux pas se connecter en tant que sois même, ça n'a aucun sens
        if($user->getId() == $subject->getId()){
            return false;
        }
        //l'utilisateur doit avoir le ROLE_ADMIN
        if (!$this->security->isGranted('ROLE_ADMIN')) {
            return false;
        }
        //Impossible si je ne suis pas SUPERADMIN et que le sujet l'est
        if (!$this->security->isGranted('ROLE_SUPERADMIN') && $this->securizer->isGranted($subject, 'ROLE_SUPERADMIN')) {
            return false;
        }
        //sinon c'est ok.
        return true;
    }

}

Tout est maintenant prêt est fonctionnel. Pour résumer en appliquant la seconde configuration YAML indiqué dans cette article et en créant les fichiers correspondant aux deux portions de code suivants, la sécurité concernant le « switch_user » de notre application est en place, et facilement personnalisable.

Symfony 5 : Utiliser la fonction « isGranted » sur n’importe quel objet utilisateur

Cet article est écrit pour la version 5 de Symfony mais à priori il devrait aussi fonctionner au moins pour les version 3 et 4 du framework

Sous Symfony la fonction permettant de savoir si l’utilisateur en train de parcourir l’application possède un certain rôle est « isGranted » elle est directement disponible à l’utilisation dans les templates twig, via les annotations, depuis le services « Symfony\Component\Security\Core\Security » ou directement dans les controllers de la façon suivante :

if ($this->isGranted('ROLE_ADMIN')) {
   //l'utilisateur à les droits admin
}

Par contre il n’existe pas de façon directe d’utiliser cette fonction sur un objet utilisateur différent de l’utilisateur courant.
Étant donnée le fonctionnement des rôles sous symfony, qui peuvent hériter des droits d’un autre rôle il ne faut pas utiliser un simple

if(in_array('ROLE_ADMIN', $user->getRoles())){
   //mauvaise pratique
}

En effet dans ce cas, si un utilisateur à le rôle ‘ROLE_SUPERADMIN’ qui hérite de ‘ROLE_ADMIN’ le contrôle ci dessus ne lui accordera pas les droits.
On imagine ici la hiérarchie des rôle suivante :

role_hierarchy:
    ROLE_ADMIN
: ROLE_USER
    ROLE_SUPERADMIN
: ROLE_ADMIN

Pour résoudre ce problème, nous allons créer la fonction « isGranted » prenant en paramètre un utilisateur et le rôle à vérifier dans un service « Securizer », je vous le livre directement prêt à utiliser :

namespace App\Service;

use App\Entity\User;//l'entité user de notre aplication
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;

class Securizer {

    private $accessDecisionManager;

    public function __construct(AccessDecisionManagerInterface $accessDecisionManager) {
        $this->accessDecisionManager = $accessDecisionManager;
    }

    public function isGranted(User $user, $attribute, $object = null) {
        $token = new UsernamePasswordToken($user, 'none', 'none', $user->getRoles());
        return ($this->accessDecisionManager->decide($token, [$attribute], $object));
    }

}

Grâce à la configuration par défaut de Symfony on peux sans plus attendre utiliser ce service dans notre application et vérifier correctement les droits de n’importe quel utilisateur en prenant bien en compte la hiérarchie des rôles configuré dans le fichier « security.yaml ». Par exemple dans un controller on peux désormais faire :

public function monController(App\Service\SecurizerSecurizer $securizer) {
   //récupération d'un utilisateur $user de n'importe quelle manière, par exemple via une requête en base de données.
   //verification des droits
   if($securizer->isGranted($user, 'ROLE_ADMIN')){
       //l'utilisateur à les droits admin
   }
}

Il y’a sans doute de nombreux cas ou ce besoin de contrôle des rôles sur un utilisateur autre que celui connecté se fait sentir (d’où la mise en place de cette fonctionnalité sous la forme d’un service), particulièrement lors de l’utilisation de Voters pour sécuriser la gestion des droits de l’application. Dans mon cas j’ai d’abord eu besoin de cette fonctionnalité pour contrôler finement la possibilité de se « connecter en tant que » (switch_user) pour mes administrateurs.
Voir les détails dans le prochain article : Limiter la possibilité de « switch_user » en fonction du rôle de l’utilisateur cible.

Source

NodeJs et ExpressJs : Générer des PDF contenant des tableaux avec pdfmake

Si l’on souhaite générer des PDF « riches » sur un projet utilisant NodeJs et ExpressJs, en particulier des PDF affichant des tableaux, le plus simple est d’utiliser pdfmake. On commence donc par installer la dépendance :

npm install pdfmake

La suite est juste un exemple concret d’utilisation sur une URL affichant directement le document PDF. Car je n’ai pas trouvé la documentation claire à ce sujet. Une fois que vous avez cette base il devient simple de mettre en page les éléments comme on le souhaite en utilisant les exemples de la documentation

router.get('/pdf', function (req, res) {
    //on importe les font à utiliser, voir https://pdfmake.github.io/docs/fonts/standard-14-fonts/ pour les font disponible par défaut
    var fonts = {Helvetica: {normal: 'Helvetica', bold: 'Helvetica-Bold', italics: 'Helvetica-Oblique', bolditalics: 'Helvetica-BoldOblique'}, };
    //exemple de contenu du PDF
    var dd = {
        //les differents contenus
        content: [
            {
                //en premier 2 colonnes contenant une imag et un texte
                columns: [
                    {image: './chemin/vers/mon-image.jpg', width: 100},//attention si l'image n'existe pas ça plante
                    //un texte avec différents style
                    {text: [
                            'MON TITRE ',
                            {text: 'N° 999', color: 'red'}//on indique directement la proriété de style color
                        ],
                        style: 'titleStyle'//style définis plus bas
                    }
                ]
            },
            '\n\n', //sauts de ligne
            {text: 'titre du tableau', style: 'tableHeader'},
            {style: 'tableExample',
                table: {
                    body: [
                        [{text: 'titre de colonne 1', style: 'tableHeader'}, {text: 'titre de colonne 2', style: 'tableHeader'}, ],
                        ['contenus de ma colonne', 'un autre contenu'],
                    ]
                }
            },
        ],
        //les différents styles à appliquer
        styles: {
            tableExample: {margin: [0, 5, 0, 15]},
            tableHeader: {bold: true, fontSize: 12, color: 'black'},
            titleStyle: {bold: true, fontsize: 14, alignment: 'right'}
        },
        defaultStyle: {font: 'Helvetica', columnGap: 20}
    };
    //on apelle pdfmake
    var PdfPrinter = require('pdfmake/src/printer');
    //avec les fonts à utiliser
    var printer = new PdfPrinter(fonts);
    //on passe notre contenu
    let pdf = printer.createPdfKitDocument(dd);
    //on termine et on envoie les bon headers
    pdf.pipe(res);
    pdf.end();
    res.statusCode = 200;
    res.setHeader('Content-type', 'application/pdf');
});

CodeIgniter : Upload multiple de fichiers

Cet article présente comment gérer des « uploads » multiple de fichier à travers les formulaires sous codeIgniter. Le code fonctionnera pour un champ input « file » ayant l’attribut multiple comme ceci :

<input type="file" multiple="multiple" name="fichiers[]">

Ou pour une liste de champ input « file » unique comme cela :

<input type="file" name="fichiers[]">
<input type="file" name="fichiers[]">
...

Comme on peux le voir, le nom de l’input doit être sous le format d’un tableau « [] ». Dans notre exemple on le nomme « fichiers ». Ci-dessous le code commenté permettant de traiter les différents fichiers envoyer par le formulaire :

$fieldName = 'fichiers';//nom du champ input file
//ce champs doit exister et avoir un élément "name" sous forme de tableau
if ($_FILES && isset($_FILES[$fieldName]) && is_array($_FILES[$fieldName]['name'])) {
  //dans ce cas on boucle sur chaque élément du tableau
  foreach ($_FILES[$fieldName]['name'] as $key => $name) {
      if ($name) {
          //on copie les variables de l'élément courant dans le tableau $_FILES avec comme nom arbitraire "file_temp". Cela permettra à codeigniter de le traiter comme un champ file simple.
          $_FILES['file_temp']['name'] = $_FILES[$fieldName]['name'][$key];
          $_FILES['file_temp']['type'] = $_FILES[$fieldName]['type'][$key];
          $_FILES['file_temp']['tmp_name'] = $_FILES[$fieldName]['tmp_name'][$key];
          $_FILES['file_temp']['error'] = $_FILES[$fieldName]['error'][$key];
          $_FILES['file_temp']['size'] = $_FILES[$fieldName]['size'][$key];
          //on met en place la configuration pour l'upload du fichier, comme on l'aurait pour n'importe quel input file sous codeigniter
          $config = [];
          $config['upload_path'] = './uploads/mon_dossier/';//dossier d'upload
          $config['allowed_types'] = ['pdf', 'doc', 'docx', 'txt', 'odt'];//types de fichiers autorisé
          $config['file_name'] = 'nom_unique_' . date('YmdHis') . '_' . rand(1000, 9999);//renommage du fichier
          //ligne les plus importantes : ne fonctionnera pas avec l'habituel "$this->load->library('upload', $config);"
          $this->load->library('upload');
          $this->upload->initialize($config);
          //on traite notre fichier dans "file_temp" et on vérifie si il y'a des erreurs.
          if (!$this->upload->do_upload('file_temp')) {    
              $msg_error = $this->upload->display_errors();
              //traiter le message d'erreur comme il vous convient
          } else {
              //tout c'est bien passé vous pouvez récupérer les informations du fichiers de cette façon
              $fichier = $this->upload->data();
              //$fichier['file_name'] -> nom du fichier
              //$fichier['client_name'] -> nom d'origine du fichier
              //traiter la réussite pour ce fichier comme il vous convient.
          }
      }
  }
}

Une autre petite astuce, pour la validation d’un fichier obligatoire, dans le cas d’un input « file » simple dans cette exemple :

<input type="file" name="fichier" required="required">

Les règles de validation codeigniter ne fonctionne pas pour les input de ce type, par exemple ceci ne fonctionnera pas :

//inutile (sera toujours en erreur)
$this->form_validation->set_rules('fichier', 'Fichier', ['required']);

Et n’oubliez pas que le fait d’ajouter l’attribut « required » en html n’est pas suffisant pour rendre un champ obligatoire. La règle d’or étant qu’il ne faut jamais faire confiance au données en provenance de l’extérieur et toujours faire une validation coté serveur. Donc le « bricolage » pour intercepter l’erreur avec codeigniter est d’utiliser la manière suivante :

//le nom du champ est "fichier". On vérifie qu'il existe et n'est pas vide
if (isset($_FILES['fichier']) empty($_FILES['fichier']['name'])) {
    $this->form_validation->set_rules('fichier', 'Fichier', ['required']);
}

Au passage, pour proposer l’upload de fichier multiples de manière « sexy » à vos utilisateurs (avec prévisualisation des images entre autre) vous pouvez utilisez le plugin javascript suivant file-upload-with-preview.

WordPress : Afficher des vidéos à partir du lien d’un fournisseur externe grâce à la fonction « wp_oembed_get »

Connaissez vous la fonction « wp_oembed_get » de wordpress ? Ce n’était pas mon cas il y’a peu de temps, et je n’ai pas tout de suite trouvé la solution de ce que je voulais faire, alors que cette fonction existante fait le travail.

Avec cette fonction à partir d’un lien (en provenance d’un champ personnalisé par exemple) wordpress cherche à créer le lecteur « embarqué » de la ressource visé par ce lien. Dit plus clairement à partir d’un lien Youtube il affiche le lecteur youtube, idem à partir d’un lien vimeo ou dailymotion. Et ceci marche avec beaucoup de service, et pas seulement des vidéos. C’est le cas par exemple pour flickr, twitter, soundcloud, instagram…etc

La fonction prend l’url en premier paramètre et optionnellement en second un tableau « height » et ou « width » pour spécifier la hauteur/largeur du document à affiché

Pour l’utiliser rien de plus simple :

$url = 'https://www.youtube.com/watch?v=-lZdBT2Zl-A';
$embed = wp_oembed_get($url, ['width'=>450]);
//si pas d'erreur on affiche
if($embed){
    echo $embed;
}

Prestashop : Modifier la liste d’un « AdminController » grâce au hook « action’ . $this->controller_name . ‘ListingResultsModifier »

Après avoir vu comment créer les base d’un module CRUD prestashop, puis quelques astuces pour adapter les fonctions en provenance du « ModuleAdminControllerCore » penchons nous à présent sur un besoin plus précis : Modifier le rendu « liste » de ce même module.

L’objet « ModuleTestTableTest » de notre module contient un champ « champ_int_test » qui comme son non l’indique va contenir un nombre entier. Comment faire si ce nombre correspond à la description d’un état (« En attente », »Traitement en cours », « Finalisé ») et que l’on veux afficher cette description compréhensible dans la liste plutôt que le nombre brut ? C’est un exemple simple qui permet de comprendre la façon de modifier ces listes, ensuite tout est possible !

A chaque fois qu’une liste est rendu (appel de la fonction « getList » du « AdminController » natif de prestashop, le hook suivant est exécuté : « ‘action’ . $this->controller_name . ‘ListingResultsModifier' » donc dans le cas de notre « moduleTest » et particulièrement de son Controller « AdminModuleTestController » il s’agira du hook « ActionAdminModuleTestListingResultsModifier ». Nous déclarons donc ce hook dans la fonction « __construct » de notre fichier « /moduletest/moduletest.php », à la suite de ce que l’on à déja :

        //hooks
        //modifications de la liste du "AdminModuleTest"
        $this->registerHook('ActionAdminModuleTestListingResultsModifier');

Nous devons ensuite créer dans ce même fichier la fonction qui va gérer ce hook. Voici son code ci-dessous. Elle reçoit un tableau en paramètre, ce tableau contient en particulier une clé « list » qui contient lui même un tableau de toutes les lignes de la liste. On va ainsi pouvoir boucler dessus et les modifier. Il contient aussi une clé « list_total » contenant ne nombre total de ligne, nous ne l’utilisons pas dans cette exemple.

    public function hookActionAdminModuletestListingResultsModifier($params) {
        //pour chaque ligne de la liste
        foreach ($params['list'] as $key => $row) {
            //si le numéro "champ_int_test" est inconnu dans la liste on ne change rien pour garder l'affichage du numéro d'origine
            //par contre si le numero correspond a une description de notre tableau "list_descriptions_int_test" on remplace le numéro par cette description
            if (isset(ModuleTestTableTest::$list_descriptions_int_test[$row['champ_int_test']])) {
                $params['list'][$key]['champ_int_test'] = ModuleTestTableTest::$list_descriptions_int_test[$row['champ_int_test']];
            }
        }
    }

Bien sur pour ne pas avoir d’erreur il nous faut créer dans notre fichier « /moduletest/classes/ModuleTestTableTest.php » la liste des descriptions correspondant au numéros. Rien de plus simple :

    public static $list_descriptions_int_test = array(
        0 => 'Initialisation',
        1 => 'En attente',
        2 => 'En traitement',
        3 => 'Terminé',
        100 => 'Echec'
    );

Prestashop : Comment modifier l’affichage et les actions d’un « ModuleAdminControllerCore »

Nous avons vu dans l’article précédent les bases d’un module Prestashop. Nous allons voir dans cet article comment faire des choses un peu plus poussé en modifiant notre controller qui étend « ModuleAdminControllerCore ». En continuant sur le module de test précédent nous seront donc situé dans le fichier « moduletest/controller/admin/AdminModuleTest.php »

Ajouter des boutons dans la toolbar
Par défaut la « header toolbar » contient seulement le bouton « aide », voici comment y ajouter des boutons. Nous ajoutons ici un bouton vers notre formulaire d’ajout (un bouton existe déjà dans la « toolbar » de la liste, mais il sera plus visible à ce niveau).

    public function initPageHeaderToolbar() {
        //Bouton d'ajout
        $this->page_header_toolbar_btn['new'] = array(
            'href' => self::$currentIndex . '&add' . $this->table . '&token=' . $this->token,
            'desc' => $this->module->l('Ajouter un test'),
            'icon' => 'process-icon-new'
        );
        parent::initPageHeaderToolbar();
    }

Bloquer une action existante par défaut
Par défaut différentes actions existe sur votre objet (ajouter, modifier, supprimer) même si les bouton ne sont pas présent les pages existent et sont accessible. Voici comment bloquer les droits d’accès :

    public function access($action, $disable = false) {
        //suppression des droit d'accés à la page d'ajout / edition
        if (in_array($action, array('add', 'edit'))) {
            return false;
        }
        return parent::access($action, $disable);
    }

Et maintenant la petite astuce pour ne pas afficher le bouton d’ajout dans la « toolbar » de la liste

    //suppression du bouton "ajouter"
    public function initToolbar() {
        parent::initToolbar();
        unset($this->toolbar_btn['new']);
    }

Permettre d’ajouter des case à cocher pour supprimer des lignes « en masse »
Dans notre fonction « __construct » il faut ajouter :

        //corps de la fonction
        //...
        //ajout de l'option suppression de masse
        $this->bulk_actions = array(
            'delete' => array(
                'text' => $this->l('Delete selected'),
                'icon' => 'icon-trash',
                'confirm' => $this->l('Delete selected items?'),
            ),
        );

Modifier le comportement d’une action
Il est facile de modifier le comportement des actions existantes (ajout, modification, suppression) en remplaçant (override) leur méthode. Ci dessous par exemple on modifie l’action de « suppression d’une ligne » pour supprimer aussi un fichier ayant l’id de la ligne si il existe.

    public function processDelete() {
        //suppression classique
        $res = parent::processDelete();
        //on appelle notre fonction de suppression du fichier
        $this->deleteFile($res);
        return $res;
    }

    //la fonction de suppression du fichier
    private function deleteFile($obj) {
        //si pas d'erreur
        if ($obj) {
            if (file_exists('/chemin/du/fichier/' . $obj->id)) {
                unlink('/chemin/du/fichier/' . $obj->id);
            }
        }
    }

Attention si vous voulez que l’action « suppression en masse » réalise aussi les suppression de fichiers, il faut alors adapter sa méthode.

    protected function processBulkDelete() {
        //pour chaque ligne à supprimer
        foreach ($this->boxes as $id) {
            //je récupère l’objet correspondant et j'utilise ma fonction de suppression du fichier
            $obj = new $this->className($id);
            $this->deleteFile($obj);
        }
        //action d'origine
        return parent::processBulkDelete();
    }

Ajouter du contenu au dessus ou en dessous de la liste
Pour personnaliser la page, par exemple ajouter un bloc de contenu avec une variable personnalisé et un bouton au dessus de la liste, nous allons utiliser un template. Il faut créer le fichier « moduletest/views/templates/admin/prev-content.tpl » qui contiendra ce code :

<div class="panel col-lg-12">
    <label>Variable custom :</label> {$variable_custom}
    <hr/>
    <button class="btn btn-primary">Bouton d'exemple</button>
</div>

Puis adapter dans notre controller la fonction « initContent » :

    public function initContent() {
        parent::initContent();
        //on charge notre template custom auquel on assigne une "variable_custom"
        $this->context->smarty->assign(array('variable_custom' => 'nanani-nanana'));
        $prevContent = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'moduletest/views/templates/admin/prev-content.tpl');
        //que l'on place devant le contenu pr défaut
        $this->context->smarty->assign(array('content' => $prevContent . $this->content));
    }

On peux aussi faire une vue complètement personnalisé, par exemple en utilisant le page « details ». En modifiant notre fonction « renderList » on ajoute un bouton « détails » pour chaque ligne.

    public function renderList() {
        //bouton détails et suppression
        $this->addRowAction('details');
        $this->addRowAction('delete');
        return parent::renderList();
    }

Et on adapte « initContent » qui devient :

    public function initContent() {
        if ($this->display == 'details') {
            //chargement de l'objet
            $this->object = $this->loadObject();
            //que l'on transmet au template
            $this->context->smarty->assign(array('object' => $this->object));
            //récupération du template perso
            $content = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'moduletest/views/templates/admin/details.tpl');
            $this->context->smarty->assign(array('content' => $content));
        } else {
            parent::initContent();
            //on charge notre template custom auquel on assigne une "variable_custom"
            $this->context->smarty->assign(array('variable_custom' => 'nanani-nanana'));
            $prevContent = $this->context->smarty->fetch(_PS_MODULE_DIR_ . 'moduletest/views/templates/admin/prev-content.tpl');
            //que l'on place devant le contenu pr défaut
            $this->context->smarty->assign(array('content' => $prevContent . $this->content));
        }
    }

De la même façon que précédemment on créer le template « moduletest/views/templates/admin/details.tpl » et pour l’exemple ici on y affiche de façon brute notre objet :

<div class="panel col-lg-12">
    <pre>{$object|print_r}</pre>
</div>

Et ce sera pas mal pour commencer.

Prestashop : Les bases d’un module CRUD

Cet article va présenter comment créer un « module de test » pour prestashop. L’objectif étant d’avoir une base de code sur laquelle on pourra partir pour ensuite répondre à des demandes plus concrètes. Ce module va créer une table dans la base de donnée et ajouter un menu dans le tableau d’administration de prestashop. Lors du clic sur ce menu on aura la possibilité de lire la table correspondante, y insérer/modifier ou supprimer des lignes. (CRUD)

Cet article contiendra peu d’explications (en dehors des commentaires dans le code) car il va surtout me servir de référence pour de prochains articles. Vous pouvez aussi consulter cet artice (en anglais) un peu plus détaillé : https://www.amauri.eng.br/en/blog/2016/03/developing-a-simple-module-with-crud-for-prestashop/

Pour commencer nous créons dans le dossier « modules » de prestashop notre dossier « moduletest » et dans ce dossier le fichier « moduletest.php » suivant :

//pour éviter un accès direct à ce fichier.
if (!defined('_PS_VERSION_')) {
    exit;
}

//on appelle le fichier de ce module "/classes/ModuleTestTableTest.php" que l'on va créer dans la partie suivante.
require_once dirname(__FILE__) . '/classes/ModuleTestTableTest.php';

class Moduletest extends Module
{
    //constructeur du module avec les informations à personnaliser.
    public function __construct(){
        $this->name = 'moduletest';
        $this->tab = 'administration';
        $this->version = '0.1.0';
        $this->author = 'auteur de test';
        $this->need_instance = 0;
        $this->ps_versions_compliancy = array('min' => '1.7', 'max' => _PS_VERSION_);

        parent::__construct();

        $this->displayName = $this->l('Nom du module de test');
        $this->description = $this->l('Description du module de test.');
    }

    //fonction d'installation du module
    public function install(){
        return parent::install() && $this->installSql() && $this->installTab();
    }

    //fonction de désinstallation du module
    public function uninstall(){
        return parent::uninstall() && $this->uninstallSql() && $this->uninstallTab();
    }

    //création de la table dans la base de données.
    protected function installSql(){
        $sqlCreate = "CREATE TABLE `" . _DB_PREFIX_ . ModuleTestTableTest::$definition["table"] . "` (
                `"
. ModuleTestTableTest::$definition["primary"] . "` int(11) unsigned NOT NULL AUTO_INCREMENT,
                `champ_varchar_test` varchar(255) DEFAULT NULL,
                `champ_date_test` DATETIME NOT NULL,
                `champ_int_test` int(11) unsigned NOT NULL,
                PRIMARY KEY (`"
. ModuleTestTableTest::$definition["primary"] . "`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8;"
;
        return Db::getInstance()->execute($sqlCreate);
    }

    //suppression de la table dans la base de données
    protected function uninstallSql(){
        $sql = "DROP TABLE " . _DB_PREFIX_ . ModuleTestTableTest::$definition["table"];
        return Db::getInstance()->execute($sql);
    }

    //création de l'onglet dans le menu de l'administration
    protected function installTab(){
        $tab = new Tab();
        $tab->class_name = 'AdminModuleTest';
        $tab->module = $this->name;
        $tab->icon = 'settings_applications';
        $tab->id_parent = (int) Tab::getIdFromClassName('DEFAULT');
        //
        $languages = Language::getLanguages();
        foreach ($languages as $lang) {
            $tab->name[$lang['id_lang']] = $this->displayName;
        }
        try {
            $tab->save();
        } catch (Exception $e) {
            echo $e->getMessage();
            return false;
        }

        return true;
    }

    //suppression de l'onglet dans le menu de l'admnistration.
    protected function uninstallTab(){
        $idTab = (int) Tab::getIdFromClassName('AdminModuleTest');
        if ($idTab) {
            $tab = new Tab($idTab);
            try {
                $tab->delete();
            } catch (Exception $e) {
                echo $e->getMessage();
                return false;
            }
        }
        return true;
    }
}

Créons maintenant le fichiers « /classes/ModuleTestTableTest.php » appelé au début du fichiers précédent. Ce fichier va hériter de « ObjectModel » de prestashop afin que l’on puisse gérer les données de façon « native » par la suite. Il va simplement servir à définir la table et les champs que notre module va ajouter à la base de données, et permettre ensuite de manipuler ces données.

//on définis les champs correspondant à ceux utilisé dans la fonction "installSql" du fichier "moduletest.php"
class ModuleTestTableTest extends ObjectModel {
    public $id;
    public $champ_varchar_test;
    public $champ_date_test;
    public $champ_int_test;
    public static $definition = array(
        'table' => 'module_test',
        'primary' => 'id_module_test',
        'multilang' => false,
        'fields' => array(
            'champ_varchar_test' => array(
                'type' => self::TYPE_STRING,
                'required' => true
            ),
            'champ_date_test' => array(
                'type' => self::TYPE_DATE,
                'required' => true
            ),
            'champ_int_test' => array(
                'type' => self::TYPE_INT,
                'required' => true
            ),
        )
    );
}

Il nous faut maintenant créer le « ModuleAdminControllerCore » nous permettant de gérer le données du module. Il nous faut créer le fichier « /controllers/admin/AdminModuleTest.php » (voir le nom indiqué dans les fonctions « installTab » et « uninstallTab » du fichier « moduletest.php ». Ce fichier hérite de « ModuleAdminControllerCore » et il nous reste peu de chose à faire pour que tout fonctionne :

//on appelle ici aussi notre classe "ObjectModel" que l'on va utiliser.
require_once _PS_MODULE_DIR_ . 'moduletest/classes/ModuleTestTableTest.php';

class AdminModuleTestController extends ModuleAdminControllerCore {

    //configuration de l'objet a utilisé et des champ à affiché
    public function __construct() {
        $this->bootstrap = true; //Gestion de l'affichage en mode bootstrap
        $this->table = ModuleTestTableTest::$definition['table']; //Table de l'objet
        $this->identifier = ModuleTestTableTest::$definition['primary']; //Clé primaire de l'objet
        $this->className = ModuleTestTableTest::class; //Classe de l'objet
        $this->lang = false; //Flag pour dire si utilisation de langues ou non
        $this->_defaultOrderBy = ModuleTestTableTest::$definition['primary'];
        //Appel de la fonction parente
        parent::__construct();
        //Liste des champs de l'objet à afficher dans la liste
        $this->fields_list = array(
            'id_module_test' => array(//nom du champ sql
                'title' => $this->module->l('ID'), //Titre
                'align' => 'center', // Alignement
                'class' => 'fixed-width-xs', //classe css de l'élément
            ),
            'champ_varchar_test' => array(
                'title' => $this->module->l('Texte'),
                'align' => 'left',
            ),
            'champ_date_test' => array(
                'title' => $this->module->l('Date'),
                'align' => 'left',
            ),
            'champ_int_test' => array(
                'title' => $this->module->l('Numéro'),
                'align' => 'left',
            ),
        );
    }

    //configuration du formulaire d'ajout/edition d'une ligne de la tabler
    //utiliser l'URL de votre admin + "index.php?controller=AdminPatterns" pour a liste des champs disponibles
    public function renderForm() {
        $this->fields_form = [
            'legend' => [
                'title' => $this->l('General Information'),
            ],
            'input' => [
                [
                    'type' => 'text',
                    'label' => $this->l('Texte'),
                    'name' => 'champ_varchar_test',
                    'required' => true,
                ],
                [
                    'type' => 'datetime',
                    'label' => $this->l('Date'),
                    'name' => 'champ_date_test',
                    'required' => true,
                ],
                [
                    'type' => 'text',
                    'label' => $this->l('Numéro'),
                    'name' => 'champ_int_test',
                    'required' => true,
                ],
            ],
            'submit' => [
                'title' => $this->l('Save'),
            ],
        ];
        return parent::renderForm();
    }

    //permet d'ajouter le bouton de suppression pour chaque ligne
    public function renderList() {
        $this->addRowAction('delete');
        return parent::renderList();
    }

}

Le module de type CRUD est maintenant opérationnel, on va regarder comment améliorer tout ça dans des articles à venir.