Prestashop : Modules gratuits pour optimiser votre seo

Dans l’ensemble de mes derniers projets prestashop, j’ai été confronté à la mise en place de redirections et la nécessiter d’édition du fichier robots.txt

La bonne nouvelle c’est qu’il existe des modules gratuits pour cela et qu’il font très bien le job pour toutes les versions de prestashop.
Il est directement possible d’éditer ces fichiers depuis l’administration, ce qui est rapide et  donne la main au webmaster pour les gérer directement.

C’est l’occasion pour moi de les partager :-), je les inclus dorénavant dans  mon installation de base.

Module édition de .htaccess :

htaccess editor

https://dh42.com/free-prestashop-modules/prestashop-htaccess-module/

Module édition du fichier robots.txt :

Robots.txt editor

https://dh42.com/free-prestashop-modules/prestashop-robots-txt-module/

Pas de commentaires

Publier des releases github via un script

Pour une fois un article qui n’est pas lié directement à une technologie en particulier , mais à la plateforme de sources github.
J’ai plusieurs modules qui sont disponibles sur cette plateforme et pour lequel un clone du dépôt ne permets pas d’obtenir un module fonctionnel.

Pour  cette raison il est nécessaire de créer des « releases » qui sont disponibles sur une page spécifique du dépot ( ex:  https://github.com/nenes25/eicaptcha/releases ) et sur lequel on peut mettre à disposition une archive ( ou tout autre fichier ) associé à la release à télécharger :

Ei captcha releases

Il est possible de faire cela manuellement directement depuis l’interface github, mais par principe manuellement c’est plus lent et plus souvent source d’erreurs qu’un script automatique.
Nous allons donc voir comment scripter tout ça en utilisant l’api v3 de github et curl.

Sachant que dans mon cas j’ai les prérequis suivants :

Je pense qu’il est possible de faire l’ensemble de la procédure ci-dessous via un script bash, mais pour moi c’était plus facile de le faire en php.

Le script nécessitera 3 fichiers situés dans un même emplacement :

  • config.php //(optionnel)
  • create_release.sh //Fichier shell
  • create_release.php // Fichier php

Le fichier config.php contiendra uniquement les identifiants d’accès à github.
( Dans le cas d’un script privé vous pouvez tout à fait les intégrer directement dans le script )

<?php 
$github_user = 'githubuser'; 
$github_password = 'githubpassword';

Le fichier create_release.sh est très basique :
il créé le tag git et le pousse sur github puis lance le script php

#! /bin/bash
 
if [ -z "$1" ]
  then
    echo "Please provide version number"
fi
 
version=$1
 
#Création du tag git et on le pousse en ligne 
git tag $version
git push --tags
 
#Lancement du script php qui se charge des autres tâches
php create_release.php $version

Tout la logique de livraison de la release est réalisée dans le fichier create_release.php

<?php
require_once dirname(__FILE__).'/config.php';
 
$baseApiUrl = 'https://api.github.com/repos/userName/RepoName/'; //Url de base du dépot
 
//On vérifie qu'un numéro de release est passé au script
if ($argc < 2) { exit("Please give a release number \n"); } //On vérifie que le numéro $release = $argv[1]; echo "Check if the release exists \n"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $baseApiUrl.'releases/tags/'.$release); $curlGlobalOptions = array( CURLOPT_USERAGENT => $github_user,
    CURLOPT_USERNAME => $github_user,
    CURLOPT_PASSWORD => $github_password,
    CURLOPT_RETURNTRANSFER => true, //Response in variable
);
 
//Curl options
curl_setopt_array($ch, $curlGlobalOptions);
 
$content = curl_exec($ch);
$info    = curl_getinfo($ch);
curl_close($ch);
 
//Si le tag existe déjà inutile de continuer le script
if ($info['http_code'] == 200) {
    echo "this tag already exists \n";
    exit('end of the script');
}
 
//Sinon création de la release
echo "Creation of the release \n";
$releaseDatas = array(
    "tag_name" => $release,
    "target_commit" => 'master', // Mettre ici la branche cible
    "name" => $release,
    "body" => "Description de la release ".$relase.' see changelog',
    //Passer à true pour debug
    "draft" => false,
    "prerelease" => false,
);
 
$curlDraft = curl_init();
curl_setopt_array($curlDraft, $curlGlobalOptions);
curl_setopt($curlDraft, CURLOPT_URL, $baseApiUrl.'releases');
curl_setopt($curlDraft, CURLOPT_POSTFIELDS, json_encode($releaseDatas));
 
$draftExec = curl_exec($curlDraft);
$draftInfo = curl_getinfo($curlDraft);
 
if ($draftInfo['http_code'] == '201') {
    echo "Release created with success \n";
} else {
    exit("Error during the creation of the release \n");
}
curl_close($curlDraft);
 
//Traitement de la réponse
$draftResponse  = json_decode($draftExec);
$assetUploadUrl = str_replace('{?name,label}', '', $draftResponse->upload_url);
 
//Logique de création du fichier joint à implémenter
 
//Ajout de la pièce jointe à la release
$archive = 'test.zip';
echo "Add zip archive to release \n";
 
$curlUpload = curl_init();
curl_setopt_array($curlUpload, $curlGlobalOptions);
curl_setopt($curlUpload, CURLOPT_URL,$assetUploadUrl.'?name='.urlencode($archive));
curl_setopt($curlUpload, CURLOPT_HTTPHEADER,
    array(
    'Content-Type: application/zip' //
    )
);
curl_setopt($curlUpload, CURLOPT_POSTFIELDS, file_get_contents($archive));
$uploadExec = curl_exec($curlUpload);
$uploadInfo = curl_getinfo($curlUpload);
curl_close($curlUpload);
 
echo "The relase is published on github \n";

Pour publier la release il suffit ensuite d’appeler le script via la commande

 ./create_release.sh 2.0.2
Pas de commentaires

Mise en place d’un bouton « Faire un don » via Paypal

Un petit article non technique pour une fois pour vous signaler que comme vous l’aurez peut-être remarqué ( ou pas il n’est pas ultra-visible ^^ ),  un nouveau bouton « Faire un don » est apparu en bas de la colonne de droite.

Celui-ci vous permets à l’envie de faire un don via Paypal pour que m’encourager à continuer à publier des articles explicatifs sur les dernières versions de Prestashop et Magento 🙂
Je n’en attends pas grand chose mais si ça peut me financer une partie de mes hébergements webs ça serait top.

Promis c’est le dernier article non technique avant encore un bon moment 😉

Pas de commentaires

Prestashop 1.7 : Nettoyer la liste des modules

Dans le listing des modules Prestashop, un point m’a toujours dérangé.
Des modules partenaires et payants remontent et se mélangent aux modules installés sur le site, ce qui fait que c’est souvent le bordel pour s’y retrouver.
J’avais déjà cherché un peu comment remédier à ce point sur la version 1.6 sans trouver de solution satisfaisante …

La bonne nouvelle sur cette version 1.7, c’est que ça devient relativement facile à faire  !  ( Par contre je n’ai pas dit que c’était propre 😉 )

La modification doit être réalisée dans une partie de Prestashop qui n’est pas suchargeable, il est donc nécessaire de modifier le fichier coeur

src/Core/Addon/Module/ModuleRepository.php

Dans la fonction getList() , mettez le contenu suivant :

public function getList()
    {
        //On ne renvoie que la liste des modules sur le disque ( c'est à dire physiquement présents sur le serveur)
        return $this->getModulesOnDisk();
        /*
         Ancien Code
        return array_merge(
            $this->getAddonsCatalogModules(),
            $this->getModulesOnDisk()
        );*/
    }

Voici un aperçu de la page des modules avant  la modification

Liste des modules avant modification

Et après 🙂

Liste modules après filtre

Seuls les modules physiquement présents sur le serveur sont listés, ce qui est beaucoup plus clair et plus propre de mon point de vue.

1 commentaire

Magento 2 : Ajouter des balises canonical sur les pages cms

Par défaut avec Magento ( c’était déjà le cas pour magento 1 et ça l’est toujours pour Magento 2 ), les pages CMS sont accessibles via plusieurs urls

Ex : http://www.magento2.dev/privacy-policy-cookie-restriction-mode/ et http://www.magento2.dev/privacy-policy-cookie-restriction-mode

La seule différence étant la présence du « / » en fin de ligne.

Pour les moteurs de recherche le contenu est donc présent sur 2 pages différentes ce qui créé du duplicate content.
La solution pour corriger cette problématique est d’ajouter une balise « canonical » qui permettra de dire laquelle des urls on souhaite indexer.
De mon côté je part du postulat que c’est l’url avec un « / » à la fin.

Pour cela nous allons faire un module Hhennes_CMS avec l’arborescence suivante :

Magento Cms module structure

Voici les contenus des fichiers de déclaration du module

etc/module.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Hhennes_Cms" setup_version="0.1.1">
        <sequence>
            <module name="Mage_Cms"/>
        </sequence>
    </module>
</config>

registration.php

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Hhennes_Cms',
    __DIR__
);

La logique du module est ensuite d’ajouter un nouveau bloc qui va ajouter cette balise sur l’ensemble des pages cms.
Pour cela nous allons créer le fichier view/frontend/layout/cms_page_view.xml avec le contenu suivant :

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="head.additional">
            <block class="Hhennes\Cms\Block\Page\Canonical" name="cms_page.canonical"/>
        </referenceContainer>
    </body>
</page>

Ne reste plus qu’a configurer le code du bloc dans Block/Page/Canonical.php

<?php
 
namespace Hhennes\Cms\Block\Page;
 
use \Magento\Framework\View\Element\AbstractBlock;
 
class Canonical extends AbstractBlock {
 
    /** @var \Magento\Cms\Model\Page  */
    protected $_page;
 
    /**
     * Canonical constructor.
     * @param Context $context
     * @param array $data
     * @param \Magento\Cms\Model\Page $page
     */
    public function __construct(
        \Magento\Framework\View\Element\Context $context,
        array $data = [],
        \Magento\Cms\Model\Page $page
    )
    {
        $this->_page = $page;
        parent::__construct($context, $data);
    }
 
    /**
     * @return \Magento\Cms\Model\Page
     */
    public function getPage()
    {
        return $this->_page;
    }
 
    /**
     * Get Canonical Page Url ( with trailing / )
     */
    public function getCanonicalPageUrl()
    {
        if ($this->getPage()) {
            return $this->getUrl($this->getPage()->getIdentifier());
        } else {
            return false;
        }
    }
 
    /**
     * Display block
     * @return string
     */
    public function _toHtml()
    {
        if ( $this->getCanonicalPageUrl() ) {
            return "\n".'<link rel="canonical" href="'.$this->getCanonicalPageUrl().'"/>'."\n";
        }
 
        return '';
    }
 
}

Si vous souhaitez indexer l’url sans le slash, il vous suffit de remplacer la ligne

return $this->getUrl($this->getPage()->getIdentifier());

par

return $this->getUrl('').$this->getPage()->getIdentifier();

Une fois le module installé, la balise canonical est bien présente 🙂

Magento 2 cms canonical

 

Pas de commentaires

Prestashop : Gestion des taches cron

Contrairement à d’autres plateformes l’exécution et la planification des tâches cron sur prestashop n’a rien d’intuitif pour l’utilisateur de l’administration ou pour le développeur.
C’est assez dommage sachant qu’elles sont indispensables pour faire tourner correctement une boutique et pour raffraichir entre autre les indexes de recherches ou de la navigation à facettes.

Nous allons voir ensemble comment gérer les cron ( ou tâches planifiées ) sur la dernière version 1.7 de Prestashop. ( La logique est la même pour les anciennes versions )

Installation du module cronjobs

Prestashop propose un module gratuit « cronjobs« qui est inclu nativement pour les versions antérieures, mais celui-ci n’est plus trouvable sur la version 1.7
Sa seule limite, et que la granularité des tâches est d’une heure, ce qui est amplement suffisant pour l’essentiel des sites e-commerce basiques.

Pour l’installer il faut récupérer le code sur github : https://github.com/PrestaShop/cronjobs

Une fois le module installé dans sa configuration, il faut choisir le mode « Expert » car le mode basique qui permettait de lancer des crons via un service proposé par Prestashop, n’est plus fonctionnel c’est malheureusement inutile d’essayer de l’utiliser 🙁

Cronjobs prestashop

Le mode expert nous donne l’url à définir dans la crontab de notre serveur ( je reviendrais ultérieurement sur la configuration sur un hébergement OVH mutualisé ) .
Cette url se chargera ensuite d’exécuter l’ensemble des tâches planifiées que nous allons définir.

Ajout d’url basiques

Nous pouvons ensuite ajouter les différentes urls nécessaires au bon fonctionnement de notre boutique

  • mise à jour de la navigation à facettes
  • mise à jour des sitemaps

Via l’interface qui est proposée nativement par le module

Ajouter tâche cron

Ajout des tâches cron d’un module

Jusque la ce module fait aussi bien que l’autre module très utilisé pour les tâches crons  ( http://www.prestatoolbox.fr/modules-gratuits/115-crontab.html)
Mais pour moi le gros avantage du module natif prestashop et qu’il est possible d’ajouter automatiquement des tâches crons via des modules 🙂

Pour faire cela c’est très simple, pour l’exemple nous allons réaliser un module hh_cronuser qui souhaite exécuter une tâche planifiée toutes les heures.

Pour cela il suffit de respecter les pré-requis suivants :

  • le module doit implémenter le hook actionCronJob
  • le module doit définir sa périodicité via une fonction getCronFrequency

Pour l’ensemble du fonctionnement vous pouvez voir les commentaires dans le code complet du module ci-dessous :

class Hh_CronUser extends Module {
 
    public function __construct() {
        $this->author = 'hhennes';
        $this->name = 'hh_cronuser';
        $this->tab = 'administration';
        $this->version = '0.1.0';
        $this->bootstrap = true;
        parent::__construct();
 
        $this->displayName = $this->l('HH Cron User');
        $this->description = $this->l('HH Sample Module with cron possibilities');
        $this->dependencies = array('cronjobs'); // Dépendance à mettre en place si obligatoire
    }
 
    /**
     * Installation du module
     * @return boolean
     */
    public function install() {
        if (!parent::install() || !$this->registerHook('actionCronJob') // Hook à implémenter 
        ) {
            return false;
        }
 
        return true;
    }
 
    /**
     * Hook d'exécution de la crontab
     */
    public function hookActionCronJob() {
 
        //Exemple basique on va créer un fichier de log et insérer un contenu dès que la tache cron est appellée
        $fp = fopen(dirname(__FILE__) . '/cron.log', 'a+');
        fputs($fp, 'CALLED at ' . date('Y-m-d H:i:s'));
        fclose($fp);
 
        //Exemple plus avancé, on souhaite effectuer des taches différentes en fonction de l'heure
        $hour = date('H');
 
        switch ($hour) {
            case 07:
                //Lancement des actions du matin
                break;
 
            case 12:
                //Lancement des actions du midi
                break;
            case 18:
                //Lancement des actions du soir
                break;
            default:
                //Action par défaut
                break;
        }
    }
 
    /**
     * Information sur la fréquence des taches cron du module
     * Granularité maximume à l'heure
     */
    public function getCronFrequency() {
        return array(
            'hour' => -1, // -1 equivalent à * en cron normal
            'day' => -1, 
            'month' => -1,
            'day_of_week' => -1
        );
    }
 
}

Lors de l’installation de notre module celui-ci va automatiquement enregistré sa tâche planifiée à la fréquence souhaitée, comme vous pouvez le voir sur la capture suivante :

Crontab module

Vous remarquez qu’il n’est possible d’enregistrer qu’une seule tâche par module, mais en cas de nécessité de plusieurs tâches à des moments différents vous pouvez toujours lancer le module toutes les heures et gérer la logique et les horaires directement dans le module comme dans mon exemple 🙂

Cas spécifique hébergement mutualisé ovh :

Chez ovh un certain nombre de restriction s’appliquent et il n’est pas possible de saisir la commande données dans le module.

Pour contourner cela nous allons donc créer un fichier « cron.php » que nous allons placer dans la racine du dossier d’administration de prestashop avec le contenu suivant :

 

<?php
/**
 * Lancement des scripts cron
 * Utilise le module prestashop cronjobs
 * Permets de planifier les tâches via l'administration
 * Lancer ce script à minima toutes les heures
 */
ini_set('display_errors','on');
include dirname(__FILE__) . '/../config/config.inc.php';
$link = new Link();
$shop_url = $link->getBaseLink();
$admin_dir = basename(dirname(__FILE__));
$cron_job_token = Configuration::getGlobalValue('CRONJOBS_EXECUTION_TOKEN');
$cronUrl = $shop_url . $admin_dir . '/index.php?controller=AdminCronJobs&token=' . $cron_job_token;
echo Tools::file_get_contents($cronUrl);

Dans le manager OVH il faudra ensuite appeller l’url http://www.votre-site.com/admin-dir/cron.php toutes les heures pour que les taches crons s’exécutent 🙂

Pas de commentaires

Prestashop 1.7 : Gérer les menus dans l’administration

Dernièrement j’ai souhaité désactiver l’affichage d’un élément de menu dans le back-office de prestashop 1.7 .
Pour un utilisateur standard c’est relativement simple puisqu’il suffit de jouer avec les permissions pour afficher / masquer les différents menu, mais ce n’est pas le cas dans un rôle d’administrateur.

La page de gestion des menus qui existait dans les anciennes versions n’est plus accessible, via des liens existants ( ou alors je ne l’ai pas trouvée ).
La bonne nouvelle par contre c’est que le controlleur existe toujours.

Pour (re)faire fonctionner la gestion des menus, nous allons donc créer un nouveau module hh_adminmenu
Avec le contenu suivant :

class Hh_AdminMenu extends Module {
 
    public function __construct() {
 
        $this->name = 'hh_adminmenu';
        $this->displayName = 'HH admin menu';
        $this->tab = '';
        $this->version = '0.1.0';
        $this->author = 'hhhennes';
 
        parent::__construct();
 
        $this->displayName = $this->l('Hh Admin menu');
        $this->description = $this->l('Display again admin menu management');
        $this->ps_versions_compliancy = array('min' => '1.7.1', 'max' => _PS_VERSION_);
    }
 
    /**
     * Install Module
     * @return boolean
     */
    public function install() {
 
        if (!parent::install()) {
            return false;
        }
 
        //Ajout du liens vers la page des menus existants
        $tab = new Tab();
        $tab->class_name = 'AdminTabs';
        $tab->id_parent = Tab::getIdFromClassName('AdminAdvancedParameters');
        $languages = Language::getLanguages();
        foreach ($languages as $lang) {
            $tab->name[$lang['id_lang']] = $this->l('Menus');
        }
        try {
            $tab->save();
        } catch (Exception $e) {
            echo $e->getMessage();
            return false;
        }
 
        return true;
    }
 
    /**
     * Uninstall module
     * @return boolean
     */
    public function uninstall() {
 
        if ( !parent::uninstall() ) {
            return false ;
        }
 
        $id_tab = Tab::getIdFromClassName('AdminTabs');
        $tab = new Tab($id_tab);
        try{
            $tab->delete();
        } catch (Exception $e) {
            echo $e->getMessage();
            return false;
        }
 
        return true;
    }
 
}

L’installation du menu permettra le retour de l’élément « menus » dans les Paramètre avancés

Menu admin

La gestion des droits de cette page a été modifiée pour en interdire l’accès, il est donc également nécessaire de sucharger le controller AdminTabsController
via le fichier override/controllers/admin/AdminTabsController.php avec le contenu suivant :

class AdminTabsController extends AdminTabsControllerCore {
 
 
    /**
     * Changement du mode d'accès pour pouvoir afficher l'onglet
     * @param string $action
     * @param bool $disable
     */
    public function access($action, $disable = false)
    {
        return Profile::getProfileAccess($this->context->employee->id_profile, Tab::getCurrentTabId());       
    }
 
}

Une fois le module installé, le menu est bien de retour 🙂

Menus admins

Pas de commentaires

Prestashop 1.7 : Ajouter des champs dans le listing produit admin

A la suite de mon précédent article sur comment ajouter des champs produits dans l’administration de prestashop 1.7 : https://www.h-hennes.fr/blog/2017/10/19/prestashop-1-7-ajouter-des-champs-produit/ , nous allons à présent voir comment ajouter des champs dans le listing des produits de l’administration.

Cette page est gérée via les nouveaux controllers symfony et leur fonctionnement est donc différents des autres listing de l’administration.

Je précise tout de suite, à date d’aujourd’hui et avec la version 1.7.2.x et inférieur il n’est pas possible de réaliser cette modification sans toucher à des fichiers « coeur » puisque la surcharge des templates symfony n’est pas encore gérée via les modules ( c’est en cours d’implémentation pour les versions suivantes, je mettrais à jour mon article en conséquence )

Pour autant la solution que je propose reste assez propre les seules lignes qui seront ajoutées le seront pour ajouter un appel de hook.

Je ne détaille pas la base de la création du module qui pourra reprendre celle de l’article sur la création des champs produits.

Le module devra implémenter les hooks suivants :

  • actionAdminProductsListingFieldsModifier (hook natif )
  • displayAdminCatalogTwigListingProductFields ( hook custom)
  • displayAdminCatalogTwigProductFilter ( hook custom )
  • displayAdminCatalogTwigProductHeader ( hook custom )

Pour l’exemple nous allons ajouter un filtre sur les marques des produits, mais la logique s’applique à n’importe quel champ.

Modification des fichiers twigs pour la création des nouveaux hooks

Les templates utilisés pour le listing des produits sont les suivants :

  • src\PrestaShopBundle\Resources\views\Admin\Product\catalog.html.twig
  • src\PrestaShopBundle\Resources\views\Admin\Product\list.html.twig

Dans catalog.html.twig :

Rajouter le code suivant :

 {# Hhennes Hook Custom pour afficher des colonnes supplémentaires #}
 {{ renderhook('displayAdminCatalogTwigProductHeader') }}

Après le code :

 <th>
 {{ "Category"|trans({}, 'Admin.Catalog.Feature') }}
 {% include 'PrestaShopBundle:Admin/Product/Include:catalog_order_carrets.html.twig' with {
 'column': 'name_category'
 } %}
 </th>

Rajouter le code suivant :

 {# Hhennes Hook Custom pour afficher des filtres supplémentaires #}
 {{ renderhook('displayAdminCatalogTwigProductFilter') }}

Après le code :

 <th>
 <input
 type="text"
 class="form-control"
 placeholder="{{ "Search category"|trans({}, 'Admin.Catalog.Help') }}"
 name="filter_column_name_category"
 value="{{ filter_column_name_category }}"
 />
 </th>

Dans list.html.twig :
Rajouter le code suivant :

 {# Hhennes Hook Custom pour afficher les nouveaux champs dans le listing #}
 {{ renderhook('displayAdminCatalogTwigListingProductFields', { 'product': product }) }}

Après le code :

 <td>
 {{ product.name_category|default('') }} 
 </td>

Affichage des nouveaux champs via les hooks du fichier twig

Nos nouveaux hooks étant maintenant créé il est temps d’ajouter nos champs via les hooks du module :

hook displayAdminCatalogTwigProductHeader : Affichage de l’entête de la colonne

 /**
 * Hook personnalisé ( non core ) pour afficher le header product
 * cf. src\PrestaShopBundle\Resources\views\Admin\Product\catalog.html.twig
 * @param type $params
 */
 public function hookDisplayAdminCatalogTwigProductHeader($params)
 {
 return $this->display(__FILE__,'views/templates/hook/displayAdminCatalogTwigProductHeader.tpl'); 
 }

Contenu du fichier displayAdminCatalogTwigProductHeader.tpl :

 <th>{l s='Manufacturer' mod='hhproduct'}</th>

 

hook displayAdminCatalogTwigProductFilter : Affichage du champ html du filtre

 /**
 * Hook personnalisé ( non core ) pour afficher le filter product
 * cf. src\PrestaShopBundle\Resources\views\Admin\Product\catalog.html.twig
 * @param type $param
 */
 public function hookDisplayAdminCatalogTwigProductFilter($params)
 {
 $manufacturers = Manufacturer::getManufacturers();
 $this->context->smarty->assign(
 [
 'filter_column_name_manufacturer' => Tools::getValue('filter_column_name_manufacturer',''),
 'manufacturers' => $manufacturers,
 ]
 );
 return $this->display(__FILE__,'views/templates/hook/displayAdminCatalogTwigProductFilter.tpl');
 }

Contenu du fichier displayAdminCatalogTwigProductFilter.tpl :

 <th>
 <select name="filter_column_name_manufacturer" data-toggle="select2">
 <option value="">{l s='Manufacturer' mod='hhproduct'}</option>
 {foreach from=$manufacturers item=manufacturer}
 <option value="{$manufacturer.id_manufacturer}" 
 {if $filter_column_name_manufacturer == $manufacturer.id_manufacturer} selected="selected"{/if}>
 {$manufacturer.name}
 </option>
 {/foreach}
 </select>
</th>

 

hook displayAdminCatalogTwigListingProductFields : Affichage de la valeur du champ pour chaque produit

 /**
 * Hook personnalisé (non core) pour afficher les informations additionnelles produits )
 * cf. src\PrestaShopBundle\Resources\views\Admin\Product\list.html.twig
 * @param type $params
 */
 public function hookDisplayAdminCatalogTwigListingProductFields($params)
 {
 $this->context->smarty->assign('product',$params['product']);
 return $this->display(__FILE__,'views/templates/hook/displayAdminCatalogTwigListingProductFields.tpl');
 }

Contenu du fichier displayAdminCatalogTwigListingProductFields.tpl :

 <td>{$product.manufacturer}</td>

 

Avec ces codes nous devrions à présent obtenir l’affichage suivant :

Prestashop produt fields

C’est bien joli mais il n’y a pas de données 😉

 

Affichage et filtrage des résultats

Pour remonter ces données la bonne nouvelle c’est que le hooks natif de prestashop 1.6 fonctionne encore !
Dans le fichier \src\Adapter\Product\AdminProductDataProvider.php, le hook actionAdminProductsListingFieldsModifier est toujours appellé.
Comme vous pouvez le voir ligne 280

 // exec legacy hook but with different parameters (retro-compat < 1.7 is broken here)
 \HookCore::exec('actionAdminProductsListingFieldsModifier', array(
 '_ps_version' => _PS_VERSION_,
 'sql_select' => &$sqlSelect,
 'sql_table' => &$sqlTable,
 'sql_where' => &$sqlWhere,
 'sql_order' => &$sqlOrder,
 'sql_limit' => &$sqlLimit,
 ));

Voici donc l’implémentation du hook dans notre module :

/**
* Modification de la requête de la liste
* @param type $params
*/
public function hookActionAdminProductsListingFieldsModifier($params)
{
/**
* Rajout du fabricant
*/
//Champ sql
$params['sql_select']['manufacturer'] = [
'table' => 'm',
'field' => 'name',
'filtering' => \PrestaShop\PrestaShop\Adapter\Admin\AbstractAdminQueryBuilder::FILTERING_LIKE_BOTH
];
//Table sql
$params['sql_table']['m'] = [
'table' => 'manufacturer',
'join' => 'LEFT JOIN',
'on' => 'p.`id_manufacturer` = m.`id_manufacturer`',
];
 
//Gestion du filtre, si un paramètre post est défini ( c'est le nom du champ dans le fichier displayAdminCatalogTwigProductFilter.tpl )
$manufacturer_filter = Tools::getValue('filter_column_name_manufacturer',false);
if ( $manufacturer_filter && $manufacturer_filter != '') {
$params['sql_where'][] .= "p.id_manufacturer =".$manufacturer_filter;
}
}

Voici donc comment rajouter et filtrer les résultats de l’attribut manufacturer 🙂

Et voici le rendu final, avec le filtre qui fonctionne :

Colonne manufacturer OK

 

 

10 commentaires

Magento 2 : Commande console pour supprimer les fichiers de logs et les reports

Lors de la phase de développement sous magento 2, les dossiers contenants les fichiers de logs et de reports ont tendance à augmenter rapidement et il devient difficile de s’y retrouver.
Il est possible de supprimer facilement le contenu des ces dossiers via l’explorateur de fichier ou via des commandes shell, mais cela peut être rébarbatif ou source d’erreurs ( rm -rf sur un mauvais dossier entre autre … )

Pour simplifier ces actions nous allons donc créer 2 nouvelles commandes dans la console Magento.
Ceci nous permets de constater qu’ajouter des commandes consoles sur magento 2 c’est très simple 🙂

Le module s’appellera Hhennes_Tools et sera situé dans app/code/Hhennes/Tools/

Pour initialiser le module créer le fichier etc/module.xml avec le contenu suivant

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Hhennes_Tools" setup_version="0.1.0">
    </module>
</config>

Et le fichier de registration registration.php avec le contenu suivant :

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Hhennes_Tools',
    __DIR__
);

Dans le fichier Hhennes/Tools/etc/di.xml ajoutez le contenu suivant :

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\Console\CommandList">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="hhennes_tools_cleanreports" xsi:type="object">Hhennes\Tools\Console\Command\CleanReportsCommand</item>
                <item name="hhennes_tools_cleanlogs" xsi:type="object">Hhennes\Tools\Console\Command\CleanLogsCommand</item>
            </argument>
        </arguments>
    </type>
</config>

La logique d’ajout des commandes est situé dans ce fichier, nous ajoutons nos commandes à la liste des commandes disponible lors de l’initialisation de la classe Magento\Framework\Console\CommandList

Il ne reste plus qu’a créer les fichiers php des commandes :
Hhennes/Tools/Console/Command/CleanLogsCommand.php

<?php
 
namespace Hhennes\Tools\Console\Command;
 
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Magento\Framework\App\Filesystem\DirectoryList;
 
class CleanLogsCommand extends Command
{
 
    /** @var DirectoryList  */
    protected $_directoryList;
 
    /**
     * CleanLogsCommand constructor.
     * @param DirectoryList $directoryList
     */
    public function __construct(DirectoryList $directoryList)
    {
        $this->_directoryList = $directoryList;
        parent::__construct();
    }
 
    public function configure()
    {
        $this->setName('dev:clean:logs')
            ->setDescription('clean logs directory');
    }
 
    /**
     * @param InputInterface $inputInterface
     * @param OutputInterface $outputInterface
     * @return bool
     */
    public function execute(InputInterface $inputInterface, OutputInterface $outputInterface)
    {
 
        if (!function_exists('exec')) {
            $outputInterface->writeln('<error>exec command should be available in order to clean log directory</error>');
        } else {
            $reportDir = $this->_directoryList->getPath(DirectoryList::VAR_DIR) . DIRECTORY_SEPARATOR . 'log';
            if (is_dir($reportDir)) {
                //Suppression du contenu du dossier des logs
                exec('rm -rf ' . $reportDir . '/*');
                $outputInterface->writeln('<info>log directory clean with success</info>');
            } else {
                $outputInterface->writeln('<error>log directory does not exists</error>');
            }
        }
        return true;
    }
 
}

Hhennes/Tools/Console/Command/CleanReportsCommand.php

<?php
 
namespace Hhennes\Tools\Console\Command;
 
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Magento\Framework\App\Filesystem\DirectoryList;
 
class CleanReportsCommand extends Command
{
 
    /** @var DirectoryList  */
    protected $_directoryList;
 
    /**
     * CleanReportsCommand constructor.
     * @param DirectoryList $directoryList
     */
    public function __construct(DirectoryList $directoryList)
    {
        $this->_directoryList = $directoryList;
        parent::__construct();
    }
 
    public function configure()
    {
        $this->setName('dev:clean:reports')
            ->setDescription('clean reports directory');
    }
 
    /**
     * @param InputInterface $inputInterface
     * @param OutputInterface $outputInterface
     * @return bool
     */
    public function execute(InputInterface $inputInterface, OutputInterface $outputInterface)
    {
 
        if (!function_exists('exec')) {
            $outputInterface->writeln('<error>exec command should be available in order to clean report directory</error>');
        } else {
            $reportDir = $this->_directoryList->getPath(DirectoryList::VAR_DIR) . DIRECTORY_SEPARATOR . 'report';
            if (is_dir($reportDir)) {
                //Suppression du contenu des reports
                exec('rm -rf ' . $reportDir . '/*');
                $outputInterface->writeln('<info>report directory clean with success</info>');
            } else {
                $outputInterface->writeln('<error>report directory does not exists</error>');
            }
        }
        return true;
    }
 
}

Puis d’ installer le module via la commande :

php bin/magento module:enable Hhennes_Tools

Puis de lancer les updates :

php bin/magento setup:upgrade

 

Relancer la console et les nouvelles commandes sont disponibles 🙂

Magento 2 commands
Pour toutes les syntaxes n’hésitez pas à consulter la documentation de la console symfony, car c’est elle que Magento utilise 😉

Pas de commentaires

Prestashop 1.7 : Ajouter des champs produit

La nouvelle version 1.7 de Prestashop introduit de gros changements dans la gestion backoffice des fiches produits.
Cette page utilise les nouvelles méthodes symfony et tout les modules souhaitant ajouter des informations produits doivent se mettre à jour pour utiliser les nouvelles méthodes.
Comme d’habitude la documentation prestashop n’est pas exhaustive et je n’ai à date trouvé aucune information ni tutoriel sur le sujet.

Nous allons donc voir ensemble comment rajouter des nouveaux champs produits et les gérer dans l’administration avec Prestashop 1.7
Pour cela nous allons créer un module hhproduct.

Celui-ci ajoutera des nouveaux champs à notre entité produit.

  • custom_field
  • custom_field_lang
  • custom_field_lang_wysiwyg

Nous allons donc de surcharger l’objet Product afin de lui ajouter ces nouveaux champs.
Pour cela créer un fichier Product.php avec le contenu suivant dans le dossier override/classes du module.

<p>lors de l’installation du module ce fichier sera automatiquement placé dans le dossier des overrides par Prestashop.</p>
<p>Contrairement aux autres formulaires Prestashop les hooks spécifiques d’ajout produit sont situés dans un fichier twig,s situé sur le chemin suivant :<br ?–>
src/PrestaShopBundle/Resources/views/Admin/Product/form.html.twig
Les hooks disponibles sont les suivants :

  • displayAdminProductsExtra
  • displayAdminProductsMainStepLeftColumnMiddle
  • displayAdminProductsMainStepLeftColumnBottom
  • displayAdminProductsMainStepRightColumnBottom
  • displayAdminProductsQuantitiesStepBottom
  • displayAdminProductsPriceStepBottom
  • displayAdminProductsOptionsStepTop
  • displayAdminProductsOptionsStepBottom
  • displayAdminProductsSeoStepBottom ( dans le fichier src/PrestaShopBundle/Resources/views/Admin/Product/Include/form_seo.html.twig )

Vous pouvez voir leurs emplacements sur les captures ci-dessous.


prestashop-product-field-1

prestashop-product-field-2


prestashop-product-field-3


prestashop-product-field-4

Nous pouvons donc greffer notre module sur l’ensemble de ces hooks ou uniquement sur celui sur lequel vous souhaitez afficher vos champs.

Pour l’exemple nous utiliserons le hook AdminProductsMainStepLeftColumnMiddle
Dans cette fonction nous allons récupérer et afficher les nouvelles informations produit.

Comme vous pouvez le voir dans le fichier twig , la fonction récupère en paramètre l’identifiant du produit édité.

{{ renderhook('displayAdminProductsMainStepLeftColumnMiddle', { 'id_product': id_product }) }}

Voici le contenu de la fonction

/**
* Affichage des informations supplémentaires sur la fiche produit
* @param type $params
* @return type
*/
public function hookDisplayAdminProductsMainStepLeftColumnMiddle($params) {
$product = new Product($params['id_product']);
$languages = Language::getLanguages($active);
$this->context->smarty->assign(array(
'custom_field' => $product->custom_field,
'custom_field_lang' => $product->custom_field_lang,
'customer_field_lang_wysiwyg' => $product->custom_field_lang_wysiwyg,
'languages' => $languages,
'default_language' => $this->context->employee->id_lang,
)
);
 
return $this->display(__FILE__, 'views/templates/hook/extrafields.tpl');
}

Note : Pour l’instant je n’ai pas trouvé de helper spécifique pour générer le contenu du formulaire, nous allons donc devoir le réaliser à la main dans le fichier extrafields.tpl qui sera situé dans le dossier views/templates/hook/ du module

Voici son contenu :

<div class="m-b-1 m-t-1">
<h2>{l s='Custom Attribute from module' mod='hhproduct'}</h2>
 
<fieldset class="form-group">
<div class="col-lg-12 col-xl-4">
 
{*Champ Standard *}
<label class="form-control-label">{l s='my custom field' mod='hhproduct'}</label>
<input type="text" name="custom_field" class="form-control" {if $custom_field && $custom_field != ''}value="{$custom_field}"{/if}/>
 
{* Champ langue avec une structure particulière *}
<label class="form-control-label">{l s='my custom lang field' mod='hhproduct'}</label>
<div class="translations tabbable">
<div class="translationsFields tab-content">
{foreach from=$languages item=language }
<div class="tab-pane translation-label-{$language.iso_code} {if $default_language == $language.id_lang}active{/if}">
<input type="text" name="custom_field_lang_{$language.id_lang}" class="form-control" {if isset({$custom_field_lang[$language.id_lang]}) && {$custom_field_lang[$language.id_lang]} != ''}value="{$custom_field_lang[$language.id_lang]}"{/if}/>
</div>
{/foreach}
</div>
</div>
</div>
 
{* Champ wysiwyg avec TinyMce *}
<div class="col-lg-12 col-xl-12">
<label class="form-control-label">{l s='my custom lang field wysiwyg' mod='hhproduct'}</label>
<div class="translations tabbable">
<div class="translationsFields tab-content bordered">
{foreach from=$languages item=language }
<div class="tab-pane translation-label-{$language.iso_code} {if $default_language == $language.id_lang}active{/if}">
<textarea name="custom_field_lang_wysiwyg_{$language.id_lang}" class="autoload_rte">{if isset({$custom_field_lang_wysiwyg[$language.id_lang]}) && {$custom_field_lang_wysiwyg[$language.id_lang]} != ''}{$custom_field_lang_wysiwyg[$language.id_lang]}{/if}</textarea>
</div>
{/foreach}
</div>
</div>
</div>
 
</fieldset>
 
<div class="clearfix"></div>
</div>

Pour la gestion des langues,nous pouvons voir dans le fichier admin-dir/themes/default/js/bundle/product/form.js qu’il faut respecter une certaine structure d’affichage pour que le changement de langue soit géré.

function switchLanguage(iso_code) {
$('div.translations.tabbable > div > div.tab-pane:not(.translation-label-' + iso_code + ')').removeClass('active');
$('div.translations.tabbable > div > div.tab-pane.translation-label-' + iso_code).addClass('active');
}

Une fois notre module installé le résultat obtenu sera le suivant :

Champs produits supplémentaire prestashop 1.7

Pour finir voici le contenu complet du fichier hhproduct.php

<?php class HhProduct extends Module { public function __construct() { $this->name = 'hhproduct';
        $this->tab = 'others';
        $this->author = 'hhennes';
        $this->version = '0.1.0';
        $this->need_instance = 0;
        $this->bootstrap = true;
 
        parent::__construct();
 
        $this->displayName = $this->l('hhproduct');
        $this->description = $this->l('add new fields to product');
        $this->ps_versions_compliancy = array('min' => '1.7.1', 'max' => _PS_VERSION_);
    }
 
   public function install() {
        if (!parent::install() || !$this->_installSql()
                //Pour les hooks suivants regarder le fichier src\PrestaShopBundle\Resources\views\Admin\Product\form.html.twig
                || ! $this->registerHook('displayAdminProductsExtra')
                || ! $this->registerHook('displayAdminProductsMainStepLeftColumnMiddle')       
        ) {
            return false;
        }
 
        return true;
    }
 
     public function uninstall() {
        return parent::uninstall() && $this->_unInstallSql();
    }
 
    /**
     * Modifications sql du module
     * @return boolean
     */
    protected function _installSql() {
        $sqlInstall = "ALTER TABLE " . _DB_PREFIX_ . "product "
                . "ADD custom_field VARCHAR(255) NULL";
        $sqlInstallLang = "ALTER TABLE " . _DB_PREFIX_ . "product_lang "
                . "ADD custom_field_lang VARCHAR(255) NULL,"
                . "ADD custom_field_lang_wysiwyg TEXT NULL";
 
        $returnSql = Db::getInstance()->execute($sqlInstall);
        $returnSqlLang = Db::getInstance()->execute($sqlInstallLang);
 
        return $returnSql && $returnSqlLang;
    }
 
    /**
     * Suppression des modification sql du module
     * @return boolean
     */
    protected function _unInstallSql() {
       $sqlInstall = "ALTER TABLE " . _DB_PREFIX_ . "product "
                . "DROP custom_field";
        $sqlInstallLang = "ALTER TABLE " . _DB_PREFIX_ . "product_lang "
                . "DROP custom_field_lang,DROP custom_field_lang_wysiwyg";
 
        $returnSql = Db::getInstance()->execute($sqlInstall);
        $returnSqlLang = Db::getInstance()->execute($sqlInstallLang);
 
        return $returnSql && $returnSqlLang;
    }
 
    public function hookDisplayAdminProductsExtra($params)
    {
 
    }
 
    /**
     * Affichage des informations supplémentaires sur la fiche produit
     * @param type $params
     * @return type
     */
    public function hookDisplayAdminProductsMainStepLeftColumnMiddle($params) {
        $product = new Product($params['id_product']);
        $languages = Language::getLanguages($active);
        $this->context->smarty->assign(array(
            'custom_field' => $product->custom_field,
            'custom_field_lang' => $product->custom_field_lang,
            'customer_field_lang_wysiwyg' => $product->custom_field_lang_wysiwyg,
            'languages' => $languages,
            'default_language' => $this->context->employee->id_lang,
            )
           );
        /** 
         * @Todo Faire marcher le champ langue
         */
        return $this->display(__FILE__, 'views/templates/hook/extrafields.tpl');
    }
}

Le processus pourra surement être amélioré lors des prochaines mise à jour de prestashop, n’hésitez pas à remonter vos astuces.
Vous pouvez télécharger le code l’ensemble du module :hhproduct
Par contre si vous ajoutez/changez des champs il faudra réinitialiser le module à chaque fois pour la bonne prise en compte de l’override

49 commentaires
Magento certified developper
Modules Prestashop
Compte Github