Archives de l’auteur : Numa

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.

WordPress : Remplacer la fonction d’un plugin utilisant if(!function_exist())

Souvent les plugins wordpress déclarent leurs fonctions dans une condition « if(!function_exist()) » de ce type :

if (!function_exists('la_fonction_du_plugin')) {
    function la_fonction_du_plugin() {
        //code de la fonction
        //...
    }
}

Si le plugin ne propose pas de méthode permettant d’overrider le fichier contenant cette fonction ni de hook pour la modifier on se retrouve bloqué. Mais on peux tirer avantage que la fonction soit englober dans un « if(!function_exist()) » pour ne pas modifier le coeur du plugin est ainsi continuer à pouvoir profiter des mises à jour.

La solution consiste à déclarer la même fonction mais avant que le fichier du plugin qui la contient soit chargé. C’est ici que se situe l’astuce, rien de plus simple quand on sait que wordpress propose un système de « must use plugins ». Il suffit de placer un fichier php dans le dossier « /wp-content/mu-plugins » (on crée le dossier si il n’existe pas). Ce fichier sera automatiquement chargé avant tous les autres plugins. On peux placer plusieurs fichiers, peu importe leur noms qui seront alors chargé dans l’ordre alphabétique. Appelons par exemple notre fichier « load.php » et remplaçons notre fonction :

if (!function_exists('la_fonction_du_plugin')) {
    function la_fonction_du_plugin() {
        //code modifié de la fonction
        //...
    }
}

C’est tout simple, encore faut il connaitre cette possibilité et l’existence des « must use plugins ». Bien sur on peux se servir de ce fichier pour excuter n’importe quel code avant que le reste ne soit chargé (plugins, thème)

NodeJs : Exemples d’utilisation avec l’ORM Sequelize

Sequelize est un ORM pour node.js compatible avec différents moteurs de base de données comme Mysql, Sqlite…etc.
Dans cet article nous allons survoler quelques cas pratiques permettant de définir le modèle de la base, et d’effectuer des requêtes dessus.

Une fois installé nous allons commencer à structurer notre base de données en créant les différents objets la composant. Tout sera situé dans un fichier « Model.js » avec pour commencer les informations de connexion à la base (ici de type mysql) :

var Sequelize = require('sequelize');
var sequelize = new Sequelize('database', 'username', 'password', {
host: 'localhost',
dialect: 'mysql',
logging: false,//passer a true pour voir les différentes requêtes effectuées par l'ORM
});
//on exporte pour utiliser notre connexion depuis les autre fichiers.
var exports = module.exports = {};
exports.sequelize = sequelize;

On va pouvoir créer notre modèle, partons sur un cas pratique simple, ou on va avoir des utilisateurs « User » qui ont un rôle « Role ». On déclare les tables et leurs champs de la manière suivante :

/**
 * ROLE
 */

const Role = sequelize.define('role', {
    id: {type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true},
    name: {type: Sequelize.STRING(255), allowNull: false},
},
        {tableName: 'role', timestamps: false, underscored: true}//par default "tableName" serait "roles" (au pluriel), "timestamps" crée 2 champs automatique pour les dates de création et de modification (très pratique si nécessaire) et "underscored" permet de créer automatiquement des champs de "relation" entre les tables de type "role_id" plutôt que "UserId".
);
exports.Role = Role;

/*
 * USER
 */

const User = sequelize.define('user', {
    id: {type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true},
    name: {type: Sequelize.STRING(255), allowNull: false, },
    email: {type: Sequelize.STRING(255), allowNull: false, unique: true},
},
        {tableName: 'user', timestamps: false, underscored: true}
);
exports.User = User;

On veux maintenant créer une relation entre ces 2 tables. L’utilisateur à un et un seul rôle. Pour cela il suffit d’ajouter la ligne suivante :

User.belongsTo(Role);//l'utilisateur à un rôle.

Cela va créer un champ « role_id » dans la table « user » (roleId si on laisse la configuration « underscored » à false par default).
Pour générer ces tables on peux utiliser la ligne suivante (tables générées seulement si elles n’existent pas)

sequelize.sync({logging: console.log});

Voyons maintenant comment récupérer nos données à travers ces objets :

    //on importe le modèle
    var Model = require('./Model');
    //recherche de tous les utilisateurs
    Model.User.findAll().then(users => {
        //on récupère ici un tableau "users" contenant une liste d'utilisateurs
        console.log(users);
    }).catch(function (e) {
        //gestion erreur
        console.log(e);
    });

La partie import du modèle et gestion des erreurs restant toujours la même, ci-dessous des exemple plus concis de différentes requêtes.

    //requête avec critère et ordre
    Model.User.findAll({
        where: {role_id: 2}, //on veux uniquement ceux qui ont le role "2"
        order: [['name', 'ASC']] //classer par ordre alphabétique sur le nom
    }).then(users => {
       //traitement terminé...
    });

    //requête d'un utilisateur par son identifiant avec inclusion de la relation "Role"
    let id =  19; //id
    Model.User.findById(id, {
        include: [{model: Model.Role}]
    }).then(user => {
        //on peux directement afficher le nom du rôle de l'utilisateur
        console.log(user.role.name);
    });

    //exemple de création d'un utilisateur, puis de sa suppression dans la foulée. Ce qui permet de voir comment effectuer des requêtes successives.
    Model.User.create({
        name: 'Test',
        email : 'test@testmail.com'
    }).then(user => {
        return user.destroy();
    }).then(destroy => {
        //traitement terminé...
    }).catch(function (e) {
        //gestion erreur
    });

    //exemple de requête d'update d'un utilisateur
    let id = 19;//id
    Model.User.update(
            {name: 'Numa'},
            {where: {id: id}}
    ).then(user => {
        //traitement terminé...
    });

Pour la suite et voir une utilisation un peu plus poussé, nous allons apporter des modifications à notre modèle. Pour l’instant on peux récupérer le rôle d’un utilisateur, mais on voudrait aussi pouvoir faire l’inverse. C’est à dire, à partir d’un rôle connaitre la liste des utilisateurs correspondant. Pour cela il nous faut ajouter une relation « hasMany » :

Role.hasMany(User);

Exemple de requête :

    //recuperations des utilisateurs correspondants au différents rôles
    Model.Role.findAll({include: [{model: Model.User}]}).then(roles => {
        //pour chaque role on peux parcourir la liste des ses utilisateurs
        roles.forEach((role) => {
            console.log(role.name);
            role.users.forEach((user) => {
                console.log(user.name);
            });
        });
    //...
    });

Revenons à nouveau sur notre modèle, et regardons comment indiquer une référence sur la même table. Ici nos utilisateurs « Operator » peuvent avoir d’autre utilisateurs qui sont leurs « Manager ».
On veux donc une clé manager_id sur notre table user. Et comme il sera intéressants de récupérer la liste des « Operators » d’un « Manager » nous créons aussi la liaison inverse « hasMany », Voici comment faire :

User.hasMany(User, {foreignKey: 'manager_id', as: 'Operators'});//l'utilisateur peux avoir des "Operators"
User.belongsTo(User, {foreignKey: 'manager_id', as: 'Manager'});//l'utilisateur peux avoir un "Manager"

Ici lors des « include » dans nos requêtes nous devrons répéter les éléments « as ». Nous allons aussi voir que nous pouvons imbriquer ces « include », et leur appliquer des critères.

    Model.User.findAll({
        include: [
            //j'inclus les roles de mon utilisateur
            {model: Model.Role},
            //mais aussi la liste de ses "Operators" qui ont comme valeur role_id ="1" je récupére également leur propre role.
            {model: Model.User, as: "Operators", where: {role_id: '149999900000000002'}, include: [{model: Model.Role}]}
        ]
    }).then(users => {
        users.forEach((user) => {
            console.log('----');
            console.log(user.name + " : " + user.role.name);
            console.log('----');
            user.Operators.forEach((operator) => {
                console.log(operator.name + " : " + operator.role.name);
            });
        });
    });

On peux bien sur aussi effectuer des requête brutes, et utiliser l’échappement de données dans celles-ci, voici un exemple :

    //parametre à echaper
    let param_name = '%a%';
    let param_role = 1;
    //sql brut
    let sql = 'SELECT (ROUND(id / 5)+123) as nimportequoi FROM user WHERE name LIKE $1 AND role_id > $2';
    Model.sequelize.query(sql, {bind: [param_name, param_role], type: Model.sequelize.QueryTypes.SELECT}).then(results => {
        console.log(results);
    });

Ce sera tout pour ce premier article au sujet de nodeJs et Sequelize !

MapBox : Utiliser le « map matching » pour tracer un itinéraire routier à partir de points GPS

Nous allons voir dans cet article comment utiliser l’API MapBox pour faire différentes choses :
– Tracer un trajet à partir de points GPS
– Tracer un itinéraire routier à partir de ces points GPS
– Afficher des points cliquables sur la carte
– Recentrer la carte autour de ces tracés/points

Le résultat que l’on va obtenir est visible ici

Pour commencer il vous faut un « token » d’accès à l’API que vous trouverez dans votre compte MapBox.

Commençons par inclure les fichiers css et js nécessaires à l’utilisation de l’API. Dans la balise « head » de notre page :

<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.50.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.50.0/mapbox-gl.css' rel='stylesheet' />

Le html minimum pour afficher la carte dans le « body » de la page : c’est un div avec un identifiant (ici « map ») ainsi qu’une largeur et une hauteur :

<div id="map" style="width: 100%; height: 400px;"></div>

Tout est prêt, on peux attaquer le code javascript. Nous partons ici du principe que nous avons une liste de coordonnées GPS sur lesquelles on veux travailler, voici celles de mon exemple :

//la liste de nos points GPS, attention dans l'ordre [LONGITUDE, LATITUDE] (et non l'inverse)
var coords = [
    [4.141553499740439, 44.052572457451014],
    [4.143273931900012, 44.05242402365157],
    [4.14427862409957, 44.05275366184478],
    [4.145185210746604, 44.05318932120335],
    [4.143211104911643, 44.053065948966925],
    [4.141692974609214, 44.05368666292508],
    [4.142165043395835, 44.05420327703502]
];

On initialise notre map sur le div #map, et au chargement on appelle nos différentes fonctions qui effectueront les traitements voulus (fonctions que l’on va créées une par une juste après)
Les 4 fonctions sont indépendantes, vous pouvez bien sur appeler uniquement celle qui vous intéresse.

//token mapBox
var accessToken = 'VOTRE-TOKEN-MAPBOX';
mapboxgl.accessToken = accessToken;
//on initialise notre map sur le div #map
var map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v10',
    center: [4.141056, 44.050022399999996], //un centre initial [longitude, latitude] (facultatif)
    zoom: 13 //un zoom initial (facultatif)
});
//au chargement de la map
map.on('load', function () {
    //on recadre la carte en fonction de nos différents points GPS
    fitMap(map, coords);
    //on affiche le tracé reliant nos différents points GPS
    displayJourney(map, coords);
    //on affiche l'itinéraire routier correspondant à nos points GPS
    displayJourneyReshaped(map, coords);
    //on affiche des marqueurs à la position de nos points GPS, indiquant leur numéros et recentrant la carte sur eux au clic.
    placeMarkers(map, coords);
});

Pour que cela fonctionne il nous faut bien sur créer les fonctions correspondantes. On attaque avec la première « fitMap » qui va permettre de cadrer la carte afin que toutes les coordonnées qu’on lui fournit soit visible :

function fitMap(map, coords) {
    var bounds = coords.reduce(function (bounds, coord) {
        return bounds.extend(coord);
    }, new mapboxgl.LngLatBounds(coords[0], coords[0]));
    map.fitBounds(bounds, {
        padding: 30 //marge autour des points
    });
}

On passe à « displayJourney » qui trace une ligne droite entre chacun des points donnés :

function displayJourney(map, coords) {
    map.addLayer({
        "id": "journey", //identifiant unique de l'objet
        "type": "line",
        "source": {
            "type": "geojson",
            "data": {
                "type": "Feature",
                "properties": {},
                "geometry": {
                    "type": "LineString",
                    "coordinates": coords
                }
            }
        },
        "paint": {
            "line-color": "#888", //couleur de la ligne
            "line-width": 2 //epaisseur de la ligne
        }
    });
}

C’est déjà pas mal, mais on veux maintenant à la place de ces lignes droites, définir le trajet « réel » utilisable par un automobiliste (ou un cycliste / marcheur selon les paramètres) sur les chemins connus. Attention il vous faut moins de 100 points pour que cela fonctionne. Attention aussi, l’utilisation de cette fonctionnalité est limité a 1000 appel par MapBox avant de devenir payant. Il peut donc être intéressant de stoker le résultat si on doit l’afficher à plusieurs reprise.
Voici la fonction « displayJourneyReshaped » qui lance un appel ajax à l’API Map Matching de MapBox pour calculer l’itinéraire :

function displayJourneyReshaped(map, coords) {
    //on transforme nos coordonées en string pour l'appel de l'API
    var coordsString = coords.join(';');
    //choix du type d'itinéraire que l'on souhaite calculer (par exemple avec "walking" on ne fera pas le tour d'un rond point, avec "driving" si.
    var typeRoute = 'driving'; //cycling, walking, driving-traffic
    var directionsRequest = 'https://api.mapbox.com/matching/v5/mapbox/'+typeRoute+'/' + coordsString + '?geometries=geojson&access_token=' + accessToken;
    var xhr = new XMLHttpRequest();
    xhr.open('GET', directionsRequest);
    xhr.onload = function () {
        if (xhr.status === 200) {
            var response = JSON.parse(xhr.responseText);
            //on récupère la données calculé qui nous permettra d'afficher l'itinéraire
            var route = response.matchings[0].geometry;
            //add layer
            map.addLayer({
                id: 'journeyReshaped', //identifiant unique de l'objet
                type: 'line',
                source: {
                    type: 'geojson',
                    data: {
                        type: 'Feature',
                        geometry: route //utilisation de l'itinéraire
                    }
                },
                paint: {
                    'line-color': "#3399ff", //couleur de la ligne
                    'line-width': 4, //epaisseur de la ligne
                    'line-opacity': 0.7 //opacité de la ligne
                }
            });
        } else {
            //en cas d'erreur ajax
            console.log('Request failed.  Returned status of ' + xhr.status);
        }
    };
    xhr.send();
}

En bonus la fonction « placeMarkers » qui permet d’afficher des points cliquable sur la carte. Pour cela on commence par ajouter un peu de style css à notre page pour l’affichage de nos marqueurs ayant la classe « marker ». A mettre donc dans le style de votre page ou votre fichier css :

.marker {background: #888;width: 22px;height: 22px;border-radius: 50%;text-align: center;color:#fff;}
.marker:hover{background: #ff0;color:#000;}

Et enfin la fonction d’affichage et clic des différents points :

function placeMarkers(map, coords) {
    var markers = [];
    //pour chaque point GPS dans coords
    coords.forEach(function (coord, index) {
        //creation d'un div avec la classe 'marker' pour l'affichage du marker
        var el = document.createElement('div');
        el.className = 'marker';
        el.setAttribute('data-index', index);//on stocke son numéro pour l'utilisation au click
        //creer un élément pour indiquer le numéro du marquer dans celui-ci
        var content = document.createTextNode(index);
        el.appendChild(content);
        //on ajoute les marquers sur notre carte
        markers[index] = new mapboxgl.Marker({element: el}).setLngLat([coord[0], coord[1]]).addTo(map);
        //au clic sur chacun d'eux on recentre la carte sur sa position
        el.addEventListener("click", function (e) {
            map.flyTo({center: markers[e.target.dataset.index].getLngLat()});
        });
    });
}

Je pense que c’est un bon début pour travailler avec MapBox, maintenant à vous le tour !

PHP : classer un tableau d’objets avec usort

En PHP il arrive de se retrouver avec un tableau d’objets qui n’est pas ordonné comme on le voudrait, il faut dans ce cas utiliser la fonction usort qui permet de trier un tableau en utilisant une fonction de comparaison.

Imaginons nous avons un tableau « $list » d’objets « User », contenant -entre autre- les propriétés « Date » et « Name », Date étant un objet DateTime et Name un string

Pour trier par date il faut créer et appeler la fonction suivante (l’exemple peux s’appliquer à n’importe quel tri)

//on déclare la fonction de tri
orderByDate($a, $b) {
    //retourner 0 en cas d'égalité
    if ($a->date == $b->date) {
        return 0;
    } else if ($a->date < $b->date) {//retourner -1 en cas d’infériorité
        return -1;
    } else {//retourner 1 en cas de supériorité
        return 1;
    }
}
//appel de la fonction avec notre tableau $list, $list contiendra ensuite les "User" trié par date.
usort($list, "orderByDate");

Si votre fonction est dans une classe et que vous l’utilisez depuis cette même classe, il faut l’appeler comme ceci :

usort($list, array($this, "orderByDate"));

Dans le cas d’une comparaison de chaine (comme par exemple ici sur le nom) on peux simplifier la syntaxe en utilisant la fonction strcmp de la manière suivante :

//on déclare la fonction
orderByName($a, $b) {
    return strcmp($a->name, $b->name);
}
//appel de la fonction
usort($list, "orderByName");

Impossibilité de fonctionnement entre OVH et Monetico Paiement

Message d’alerte :

– Si vous utilisez un serveur mutualisé OVH placé sur le cluster 26 (avec donc l’ip de sortie suivante « 91.134.248.211 »).

– Si vous utilisez la solution de paiement « Monetico paiement » et y faite des appels automatisés depuis votre serveur (dans le cas de paiements récurrents ou partiels par exemple), c’est à dire sans utiliser directement un formulaire de paiement cliquable par l’utilisateur.

Vous risquez d’être confronté a des soucis, en effet l’adresse IP du cluster 26 d’OVH étant considérée comme à risque par Monetico paiement, les appels sont parfois complètement bloqués pendant plusieurs jours (cela arrive par intermittence, l’adresse, quand elle est débloquée passe à nouveau en alerte au bout de quelques jour, du moins cela à été le cas durant tout le mois de mai 2018)
Comme pendant la période ou j’ai été confronté au problème (plus d’un mois) personne ni chez OVH ni chez Monetico ne pouvait régler le problème, j’ai été dans l’obligation de changer d’hébergeur.

Prestashop (1.7) : Modifier des champs du formulaire de contact

Prestashop propose une page contenant un formulaire de contact afin que les visiteurs et utilisateurs de votre boutique puissent communiquer avec vous.
Cependant ce formulaire est peu personnalisable. Nous allons voir ci-dessous comment personnaliser ce formulaire. Notre exemple consistera en l’ajout d’un champ supplémentaire, mais la procédure permet permet de comprendre comment faire pour également supprimer ou modifier des champs existants.

Premièrement nous allons modifier le template du formulaire de contact afin de lui ajouter un champ.
Pour cela nous allons récupérer le template qui se situe dans votre thème (thème parent) ou bien directement dans le module « contactform » de prestashop.
Donc soit dans :

/themes/votre-theme-parent/modules/contactform/views/templates/widget/contactform.tpl

Soit dans

/modules/contactform/views/templates/widget/contactform.tpl

Et copier ce fichier dans votre theme enfant pour pouvoir le modifier proprement. C’est à dire dans :

/themes/votre-theme-enfant/modules/contactform/views/templates/widget/contactform.tpl

Vous pouvez maintenant faire vos modifications dans ce fichier. Par exemple pour ajouter un champ « Téléphone » (avec bien sur le html à adapter en fonction de votre thème et de la présentation souhaité) :

<div class="form-group row">
    <div class="col-md-12">
        <input class="form-control" name="phone" type="text" placeholder="Téléphone" value="{$contact.phone}">
    </div>
</div>

La seconde étape consiste à overrider le module contactform en étendant la classe « ContactForm » que vous trouverez dans le fichier :

/modules/contactform/contactform.php

Pour cela nous créons le fichiers suivant :

/override/modules/contactform/contactform.php

qui contiendra le code suivant (pour commencer) :

<?php

if (!defined('_PS_VERSION_')) {
    exit;
}

class ContactFormOverride extends Contactform {

//code à venir

}

On va à présent remplacer la ligne « //code à venir » en copiant telles quelles les 2 fonctions « getWidgetVariables » et « sendMessage » présentent dans le fichier d’origine :

/modules/contactform/contactform.php

On adapter les ligne de la fonction « getWidgetVariables » pour ajouter notre champs :

//...
//ajout de notre champ
$this->contact['phone'] = html_entity_decode(Tools::getValue('phone'));
//code d'origine
$this->contact['contacts'] = $this->getTemplateVarContact();
$this->contact['message'] = html_entity_decode(Tools::getValue('message'));
//etc...

Passons à la fonction « sendMessage » ou il va falloir modifier plusieurs partie du code, mais ces parties sont assez facile à repérer.
Il s’agit des 3 suivantes :

//debut de la fonction
$extension = array('.txt', '.rtf', '.doc', '.docx', '.pdf', '.zip', '.png', '.jpeg', '.gif', '.jpg');
$file_attachment = Tools::fileAttachment('fileUpload');
$message = trim(Tools::getValue('message'));
//ajout de notre champs
$phone = trim(Tools::getValue('phone'));
//...
//controle des différentes champs du formulaire
} elseif (!Validate::isCleanHtml($message)) {
    $this->context->controller->errors[] = $this->trans('Invalid message', array(), 'Shop.Notifications.Error');
} elseif (!Validate::isCleanHtml($phone)) {//contrôle de notre champ supplémentaire.
    $this->context->controller->errors[] = $this->trans('Téléphone invalide', array(), 'Shop.Notifications.Error');
}//....
$var_list = [
   '{order_name}' => '-',
   '{attached_file}' => '-',
   '{message}' => Tools::nl2br(stripslashes($message)),
   '{email}' => $from,
   '{product_name}' => '',
   '{phone}' => Tools::nl2br(stripslashes($phone)),//notre ajout
 ];

Il nous reste une dernière étape, il s’agit de modifier les templates correspondant à l’email transmis par le formulaire.
Vous pouvez retrouver ces template dans

/mails/fr/contact.html
/mails/fr/contact.txt

en adaptant la partie /fr/ selon votre language.
Vous devez copier ces fichier dans votre thème enfant :

/themes/votre-theme-enfant/mails/fr/contact.html
...

Et modifier leur contenus en ajoutant les informations concernant le nouveau champ.
Par exemple :

<span style="color:#333"><strong>Téléphone du client :</strong></span> {phone}<br /><br />

Normalement tout est bon !

Symfony 4 : Les base d’une gestion des utilisateurs (inscription, connexion, droits d’accès)

Quand on démarre le développement d’une site sous Symfony, l’une des première chose à faire concerne la gestion des utilisateurs. En effet dans la plupart des cas notre application devra permettre à des utilisateurs de s’inscrire puis de se connecter au site afin d’avoir accès à des sections particulières (espace membre, espace administrateur). Je reprends dans cet article les différents éléments à mettre en place pour avoir une base de travail fonctionnelle.

On va rendre possible une inscription (email / mot de passe), puis une connexion (idem). Une fois connecté on aura accès a un espace membre.

Nous partirons d’une installation « website » de symfony 4 lancé avec la commande

composer create-project symfony/website-skeleton my-project

La première chose à faire sera de créer la base de données, et de la configurer dans le fichier « /.env », par exemple :

DATABASE_URL=mysql://root:@localhost/ma_base

Entrons dans le vif du sujet, avec la création de l’entité « User », dans « /src/Entity/User.php ». Elle contiendra les champs email, password, isActive, roles et étendra la classe « UserInterface » de symfony. Ce qui nous donne avec les getters/setters et les méthodes obligatoires à implémenter, le code suivant :

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * @ORM\Table(name="user")
 * @UniqueEntity(fields="email")
 * @ORM\Entity()
 */

class User implements UserInterface, \Serializable {

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */

    private $id;

    /**
     * @ORM\Column(type="string", length=255, unique=true)
     * @Assert\NotBlank()
     * @Assert\Email()
     */

    private $email;

    /**
     * @Assert\NotBlank()
     * @Assert\Length(max=250)
     */

    private $plainPassword;

    /**
     * The below length depends on the "algorithm" you use for encoding
     * the password, but this works well with bcrypt.
     *
     * @ORM\Column(type="string", length=64)
     */

    private $password;

    /**
     * @ORM\Column(name="is_active", type="boolean")
     */

    private $isActive;

    /**
     * @ORM\Column(name="roles", type="array")
     */

    private $roles = array();

    public function __construct() {
        $this->isActive = true;
        // may not be needed, see section on salt below
        // $this->salt = md5(uniqid('', true));
    }

    public function getUsername() {
        return $this->email;
    }

    public function getSalt() {
        // you *may* need a real salt depending on your encoder
        // see section on salt below
        return null;
    }

    public function getPassword() {
        return $this->password;
    }

    function setPassword($password) {
        $this->password = $password;
    }

    public function getRoles() {
        if (empty($this->roles)) {
            return ['ROLE_USER'];
        }
        return $this->roles;
    }

    function addRole($role) {
        $this->roles[] = $role;
    }

    public function eraseCredentials() {
       
    }

    /** @see \Serializable::serialize() */
    public function serialize() {
        return serialize(array(
            $this->id,
            $this->email,
            $this->password,
            $this->isActive,
                // see section on salt below
                // $this->salt,
        ));
    }

    /** @see \Serializable::unserialize() */
    public function unserialize($serialized) {
        list (
                $this->id,
                $this->email,
                $this->password,
                $this->isActive,
                // see section on salt below
                // $this->salt
                ) = unserialize($serialized);
    }

    function getId() {
        return $this->id;
    }

    function getEmail() {
        return $this->email;
    }

    function getPlainPassword() {
        return $this->plainPassword;
    }

    function getIsActive() {
        return $this->isActive;
    }

    function setId($id) {
        $this->id = $id;
    }

    function setEmail($email) {
        $this->email = $email;
    }

    function setPlainPassword($plainPassword) {
        $this->plainPassword = $plainPassword;
    }

    function setIsActive($isActive) {
        $this->isActive = $isActive;
    }

}

Une fois ce fichier enregistré, on peux mettre à jour notre base de données afin d’y créer la table « User ». Pour cela on lance les 2 commandes suivantes :

php bin/console doctrine:migration:diff
php bin/console doctrine:migration:migrate

Il est temps de passer à la configuration. Rendons nous dans le fichier « /config/packages/security.yaml » et remplissons le de la façon suivante :

security:
   # encoder
    encoders
:
        App\Entity\User
:
            algorithm
: bcrypt
   
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers
:
        our_db_provider
:
            entity
:
                class
: App\Entity\User
                property
: email
       
    firewalls
:
        main
:
            pattern
:   ^/
            http_basic
: ~
            provider
: our_db_provider
            anonymous
: ~
            form_login
:
                login_path
: login
                check_path
: login
            logout
:
                path
:  /logout
                target
: /
           
    role_hierarchy
:
        ROLE_ADMIN
:      ROLE_USER
        #ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control
:
        - { path
: ^/admin, roles: ROLE_ADMIN }
        - { path
: ^/member, roles: ROLE_USER }

Comme on peux le comprendre on a mis en place la configuration pour l’encodage du mot de passe, on indique que nos utilisateurs passe par l’entité « App\Entity\User » et que le login est le champ « email ». On configure aussi les liens /login et /logout à utiliser, les différents roles possible (ROLE_USER, ROLE_ADMIN) et bien sur les droits d’accès en fonctions des rôles. Ici toutes les routes commençant par « /member » ne seront accessible que pour les membres connectés, et celle commençant par « /admin » pour les membres connecté ayant le rôle « ROLE_ADMIN ».

Pour faire fonctionner la déconnexion « logout », il faut simplement créer la route. On se rend donc dans le fichier « /config/routes.yaml » pour y mettre ce code :

logout:
    path
: /logout

Il nous reste à créer la page contenant le formulaire d’inscription, celle contenant le formulaire de connexion et tout sera fonctionnel.

Pour l’inscription, on va d’abord créer le formulaire dans un fichier à part qui sera « /src/Form/UserType.php ». J’ai ajoutés des classes css de bootstrap pour le bouton submit, car on verra plus bas que je propose d’utiliser bootstrap 4 pour l’affichage, mais il s’agit bien sur uniquement d’un exemple.

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class UserType extends AbstractType {

    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder
                ->add('email', EmailType::class)
                ->add('plainPassword', RepeatedType::class, array(
                    'type' => PasswordType::class,
                    'first_options' => array('label' => 'Mot de passe'),
                    'second_options' => array('label' => 'Confirmation du mot de passe'),
                ))
                ->add('submit', SubmitType::class, ['label'=>'Envoyer', 'attr'=>['class'=>'btn-primary btn-block']])
        ;
    }
}

Nous allons utiliser ce formulaire dans le controller « /src/Controller/RegistrationController.php ». La page permettant l’inscription sera accessible sur l’url « /register », ici on enregistre et on valide directement le compte, on peut aussi attribuer le rôle utilisateur dans ce controlleur. C’est une base qui pourras être amélioré (envoie d’un email de confirmation…etc)

namespace App\Controller;

use App\Form\UserType;
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class RegistrationController extends Controller {

    /**
     * @Route("/register")
     */

    public function registerAction(Request $request, UserPasswordEncoderInterface $passwordEncoder) {
        // 1) build the form
        $user = new User();
        $form = $this->createForm(UserType::class, $user);
        // 2) handle the submit (will only happen on POST)
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            // 3) Encode the password (you could also do this via Doctrine listener)
            $password = $passwordEncoder->encodePassword($user, $user->getPlainPassword());
            $user->setPassword($password);
            //on active par défaut
            $user->setIsActive(true);
            //$user->addRole("ROLE_ADMIN");
            // 4) save the User!
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($user);
            $entityManager->flush();
            // ... do any other work - like sending them an email, etc
            // maybe set a "flash" success message for the user
            $this->addFlash('success', 'Votre compte à bien été enregistré.');
            //return $this->redirectToRoute('login');
        }
        return $this->render('registration/register.html.twig', ['form' => $form->createView(), 'mainNavRegistration' => true, 'title' => 'Inscription']);
    }

}

Dans la foulée, on enchaine sur le controller « /src/Controller/SecurityController.php » avec la page « /login » qui permettra la connexion, cette fois on créer directement le formulaire dans le controller, cela permet de voir une autre manière de faire que pour l’enregistrement.

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class SecurityController extends Controller {

    /**
     * @Route("/login", name="login")
     */

    public function login(Request $request, AuthenticationUtils $authenticationUtils) {
        // get the login error if there is one
        $error = $authenticationUtils->getLastAuthenticationError();
        // last username entered by the user
        $lastUsername = $authenticationUtils->getLastUsername();
        //
        $form = $this->get('form.factory')
                ->createNamedBuilder(null)
                ->add('_username', null, ['label' => 'Email'])
                ->add('_password', \Symfony\Component\Form\Extension\Core\Type\PasswordType::class, ['label' => 'Mot de passe'])
                ->add('ok', \Symfony\Component\Form\Extension\Core\Type\SubmitType::class, ['label' => 'Ok', 'attr' => ['class' => 'btn-primary btn-block']])
                ->getForm();
        return $this->render('security/login.html.twig', [
                    'mainNavLogin' => true, 'title' => 'Connexion',
                    //
                    'form' => $form->createView(),
                    'last_username' => $lastUsername,
                    'error' => $error,
        ]);
    }

}

Tout ce qui concerne la partie « technique » et maintenant en place et fonctionnel. Pour aller jusqu’au bout on va aussi voir la mise en place des templates pour pouvoir tester le tout (avec un joli affichage à l’aide de bootstrap 4). Pour avoir accès à nos route « /register » et « /login » de nos controllers il nous faut créer les 2 templates appelés par ceux-ci. Les 2 sont très simples et assez proches.

« /templates/registration/register.html.twig »

{% extends 'base.html.twig' %}

{% block body %}
    {{form(form)}}
{% endblock %}

« /templates/security/login.html.twig »

{% extends 'base.html.twig' %}

{% block body %}

    {% if error %}
        <div class="alert alert-danger">
            <ul class="list-unstyled mb-0">
                <li><span class="initialism form-error-icon badge badge-danger">Error</span>
                    <span class="form-error-message">{{ error.messageKey|trans(error.messageData, 'security') }}</span>
                </li>
            </ul>
        </div>
    {% endif %}
    {{form(form)}}
{% endblock %}

Ces 2 templates dépendent de « base.html.twig », c’est à dire le fichier « /templates/base.html.twig », ci dessous je vous en met un exemple. comme vu précédemment il utilise bootstrap 4. Pour aller jusqu’au bout j’ai inclus ici un menu vers différente page de l’application : les page d’inscription/connnexion ou de déconnexion si on est connecté. Ainsi qu’une page d’accueil, et un espace membre et/ou espace admin en fonction du rôle de l’utilisateur connecté. Pour que tout fonctionne il faudra donc créer les controllers pour ces 3 pages ainsi que leur vues, on voit ça juste après, à la fin de l’article.

<!DOCTYPE html>
<html lang="{{ app.request.locale }}">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1"/>
        <title>
            {% block title %}
                {% if title is defined %}{{title}}{% else %}Title{% endif %} | Nom du site
            {% endblock %}
        </title>
        {% block stylesheets %}
            {# bootstrap #}
            <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
            {# custom #}
            <link rel="stylesheet" href="{{ asset('assets/css/styles.css') }}">
        {% endblock %}
        <link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
    </head>
    <body>
    <body id="{% block body_id %}{% endblock %}">

        {% block header %}
            <header id="header" class="bck1 border-bottom">
                <div class="container">
                    <h1>{% if title is defined %}{{title}}{% else %}Header{% endif %}</h1>
                </div>
            </header>
        {% endblock %}

        {% block nav %}
            <nav id="main-nav" class="container navbar navbar-expand-lg navbar-light bg-light border rounded mb-3">
                <ul class="navbar-nav">
                    <li class="nav-item {% if mainNavHome is defined %}active{% endif %}">
                        <a class="nav-link" href="{{path('app_homepage_index')}}">Accueil</a>
                    </li>
                    {% if is_granted('ROLE_USER') %}
                        <li class="nav-item {% if mainNavMember is defined %}active{% endif %}">
                            <a class="nav-link" href="{{path('app_member_index')}}">Espace membre</a>
                        </li>
                    {% endif %}
                    {% if is_granted('ROLE_ADMIN') %}
                        <li class="nav-item {% if mainNavAdmin is defined %}active{% endif %}">
                            <a class="nav-link" href="{{path('app_admin_homepage_index')}}">Espace admin</a>
                        </li>
                    {% endif %}
                </ul>
                <ul class="navbar-nav ml-auto">
                    {% if is_granted('ROLE_USER') %}
                        <li class="nav-item">
                            <a class="nav-link" href="{{path('logout')}}">Déconnexion</a>
                        </li>
                    {% else %}
                        <li class="nav-item {% if mainNavLogin is defined %}active{% endif %}">
                            <a class="nav-link" href="{{path('login')}}">Connexion</a>
                        </li>
                        <li class="nav-item {% if mainNavRegistration is defined %}active{% endif %}">
                            <a class="nav-link" href="{{path('app_registration_register')}}">Inscription</a>
                        </li>
                    {% endif %}
                </ul>
            </nav>
        {% endblock %}

        {% block flash %}
            {% for flash_message in app.session.flashbag.get('success') %}
                <div class="container alert alert-success" role="alert">
                    {{ flash_message }}
                </div>
            {% endfor %}
            {% for flash_message in app.session.flashbag.get('info') %}
                <div class="container alert alert-info" role="alert">
                    {{ flash_message }}
                </div>
            {% endfor %}
            {% for flash_message in app.session.flashbag.get('warning') %}
                <div class="container alert alert-warning" role="alert">
                    {{ flash_message }}
                </div>
            {% endfor %}
            {% for flash_message in app.session.flashbag.get('danger') %}
                <div class="container alert alert-danger" role="alert">
                    {{ flash_message }}
                </div>
            {% endfor %}
        {% endblock %}

        <div class="container bck1 padded">
            {% block body %}
            {% endblock %}
        </div>

        {% block footer %}
            <footer id="footer" class="bck1 border-top">
                <div class="container">
                    <h4>Footer</h4>
                </div>
            </footer>
        {% endblock %}

        {% block javascripts %}{% endblock %}
    </body>
</html>

Le layout présenté ci-dessus inclus en plus de bootstrap un fichier css custom permettant d’ajouter nos propres classe de présentation. Voici les quelques lignes ajouté pour ma part dans « /public/assets/css/styles.css »

/*base*/
body{background: #ebeff2;}
.bck1{background: #fff;}

/*generique*/
.padded{padding: 15px;}

/*header*/
header#header {margin-bottom: 20px;padding: 10px;}
header#header h1 {text-align: center;color: #5cb85c;}

/*footer*/
footer#footer{margin-top: 20px;padding: 10px;}

Et pour que nos formulaires s’affichent en suivant les conventions de bootstrap 4 on remercie symfony et sa communauté, une seule ligne de configuration suffit ! Dans « /config/packages/twig.yaml » :

twig:
    form_themes
: ['bootstrap_4_layout.html.twig']

Pour la page d’accueil le simple controller « /src/Controller/HomepageController.php » suffira :

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;

class HomepageController extends Controller {

    /**
     * @Route("/")
     */

    public function index() {
        return $this->render('homepage/index.html.twig', ['mainNavHome'=>true, 'title'=>'Accueil']);
    }

}

Rien de spécial pour la vue il suffit de créer le fichier correspandant « /templates/homepage/index.html.twig » de la même façon qu’on l’a fait pour « inscription » et « login », avec le contenu que l’on souhaite.

Le dernier points à voir, c’est la création des « espace membre » et « espace administrateur » limité selon les rôles de l’utilisateur. Il suffit pour cela, en accord avec la configuration faite dans « /config/packages/security.yaml » de faire commencer les routes par « /member » ou « /admin » selon ce que l’on souhaite. Voici pour l’espace utilisateur (fichier « /src/Controller/MemberController.php ») :

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;

/** @Route("/member") */
class MemberController extends Controller {

    /**
     * @Route("/")
     */

    public function index() {
        return $this->render('member/index.html.twig', ['mainNavMember'=>true, 'title'=>'Espace Membre']);
    }

}

Exactement pareil pour l’admin, sauf que dans un soucis d’organisation je place le fichier dans un dossier admin, ce qui donne « src/Controller/Admin/HomepageController.php : « 

namespace App\Controller\Admin;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;

/** @Route("/admin") */
class HomepageController extends Controller {

    /**
     * @Route("/")
     */

    public function index() {
        return $this->render('admin/homepage/index.html.twig', ['mainNavAdmin' => true, 'title' => 'Espace Admin']);
    }

}

Je ne rajoute pas le code des « views » car rien de particulier et donc même processus que pour la page d’accueil.

C’est terminé pour ce long article à réutiliser à souhait lors du démarrage d’un projet symfony 4

WordPress : Fonctions de gestions des utilisateurs

Ci-dessous un petit mémo de quelques fonctions permettant la gestion des utilisateurs sous WordPress

//Savoir si le visiteur est connecté en tant qu'utilisateur :
is_user_logged_in();

//Récupération de l'identifiant de l'utilisateur courant :
get_current_user_id();

//Récupération de l'objet user courant et affichage de son nom
$user = wp_get_current_user();
echo $user->display_name;

//Vérifier si l'utilisateur a un rôle (à partir de l'objet $user récupérer ci-dessus)
if (is_user_logged_in() && in_array('administrator', $user->roles)) {
    //option visible par l'administrateur
}

//Afficher l'avatar d'un utilisateur (l'utilisateur courant dans ce cas) avec une taille de 60px
echo get_avatar(get_current_user_id(), 60);

//Récupérer la valeur meta unique "nom_de_le_meta" pour l'utilisateur courant :
get_user_meta(get_current_user_id(), 'nom_de_le_meta', true);

//Enregistrer la valeur "mavaleur" pour la meta unique "nom_de_le_meta" de l'utilisateur courant :
add_user_meta(get_current_user_id(), 'nom_de_le_meta', "mavaleur", true);

WordPress : Utilisation de l’ajax dans WordPress

Voici un post d’exemple sur l’utilisation de l’ajax dans WordPress. Nous allons voir à travers la création d’un mini plugin, comment lancer une requête ajax utilisant le système conçu pour ça dans wordpress.

Le plugin sera utilisable en plaçant le shortcode

[submit-input-ajax]

dans une page. La démonstration est ici

On avait déjà vu comment créer un plugin permettant l’utilisation d’un shortcode. Pour ajouter de l’ajax il faudra ajouter certains éléments « wp_ajax_ ». Le code fonctionnel du plugin est présenté ci-dessous :

/*
  Plugin Name: Plugin action ajax
  Description: Demonstration d'un plugin effectuant une action ajax
 */


class PluginActionAjax {

    public function __construct() {
        //shortcode permettant d'afficher le formulaire avec le champs input et le bouton de soumission
        add_shortcode('submit-input-ajax', array($this, 'shortcode_submit_input_ajax'));
        //chargement des scripts à inclures (css, js...)
        add_action('wp_enqueue_scripts', array($this, 'action_ajax_scripts'));
        //appel ajax de notre fonction "process_action_ajax" si l'utilisateur est loggé "wp_ajax_" ou si il ne l'est pas "wp_ajax_nopriv_"
        add_action('wp_ajax_process_action', array($this, 'process_action_ajax'));
        add_action('wp_ajax_nopriv_process_action', array($this, 'process_action_ajax'));
    }

    /**
     * génère le formulaire lorsqu'on utilise le plugin [submit-input-ajax]
     */

    public function shortcode_submit_input_ajax($atts) {
        $html = '<form class="submit-input-ajax-container">';
        $html .= '<div><label for="submit-input-ajax">Veuillez entrez une information et valider en cliquant sur le bouton</label></div>';
        $html .= '<input type="text" id="submit-input-ajax" />';
        $html .= '<button type="submit" id="btn-submit-input-ajax" >Valider</button>';
        $html .= '</form>';
        return $html;
    }

    /**
     * traite l'appel ajax
     */

    public function process_action_ajax() {
        //verifie l'existence non vide de l'input
        if (isset($_POST['input']) && $_POST['input'] != '') {
            //n'importe quel traitement
            $message = 'Voici votre message en sha1 : ';
            $message .= sha1($_POST['input']);
            //retour success
            wp_send_json_success(['message' => $message]);
        }
        //retour erreur
        wp_send_json_error(['message' => 'Impossible !']);
    }

    /**
     * Appel des fichiers js/css nécessaires au fonctionnement
     */

    public function action_ajax_scripts() {
        global $post;
        //si notre shortcode est présent dans le post courant.
        if (is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'submit-input-ajax')) {
            wp_enqueue_style('paa-style', plugin_dir_url(__FILE__) . '/styles.css');
            //ajout du script lancant la requete ajax
            wp_enqueue_script('paa-ajax', plugin_dir_url(__FILE__) . '/script.js', array('jquery'));
            //on informe le script précédent de l'adresse "adminAjax" de wordpress
            wp_localize_script('paa-ajax', 'adminAjax', admin_url('admin-ajax.php'));
        }
    }

}
new PluginActionAjax();

Il ne reste plus qu’a créer le fichier script.js qui lancera l’appel de « process_action » lors de la soumission du formulaire généré par le shortcode. Il contient le code suivant :

jQuery(document).ready(function () {
    /*lors de la soumission du formulaire, on vérifie que le champ est replie et on appelle notre action ajax*/
    jQuery(document).on("submit", ".submit-input-ajax-container", function (e) {
        e.preventDefault();
        var input = jQuery("#submit-input-ajax").val();
        if (input == "") {
            alert("Vous devez completer le champ ! ");
        } else {
            jQuery.ajax({
                //apel de l'url ajax wordpress
                url: adminAjax,
                method: 'POST',
                data: {
                    //nom de notre action déclaré dans le plugin "wp_ajax_..."
                    action: 'process_action',
                    input: input
                },
                success: function (data) {
                    alert(data.data.message);
                },
                error: function (data) {
                    //erreur
                }
            });
        }
    });
});

Pour des explication détaillées, vous pouvez vous rendre sur cette page