Prestashop 1.7 : Ajouter une navigation à facettes dans les listings

La gestion des listings à complétement été réécrite sur Prestashop 1.7 , et la bonne nouvelle et qu’ils gèrent maintenant nativement la navigation à facettes ( Tout du moins la partie front 😉 )
Nous allons voir ensemble comment mettre en place une navigation à facette basique sur la page des nouveaux produits.

Sommaire :

  1. Fonctionnement général
  2. Affichage de facettes
  3. Tri et gestion des facettes

Cet article est plus dans une démarche d’explication du fonctionnement, que dans la réalisation d’un module purement fonctionnel, et la réalisation est assez chronophage, dans la majorité des cas il sera préférable de passer par un module qui fera cela.
( Le module de navigation à facette de Prestashop le permets uniquement sur les catégories )

Fonctionnement général

Le premier point essentiel à noter et qu’il n’est pas nécessaire de faire de surcharge, tout peut être géré via un module 🙂

Avant de voir ce qu’il faut coder, il est important de comprendre comment fonctionnent les pages de listing sur prestashop 1.7
Dans mon exemple on va utiliser le controller NewProductsController ( controllers/front/listing/NewProductsController.php ) qui gère les nouveaux produits.

Lors de son initialisation il appelle la fonction doProductSearch de la classe parente ProductListingFrontController

    public function init()
    {
        parent::init();
        $this->doProductSearch('catalog/listing/new-products');
    }

Celle-ci appelle la fonction getProductSearchVariables ( j’omets volontairement le cas getAjaxProductSearchVariables qui est parlant puisqu’il rajoute des information spécifiques uniquement lors d’un appel ajax )

Le code est un peu long, mais toute la logique des controller listing sous Prestashop est dans cette fonction :
Je laisse les commentaires de base en anglais qui sont plutôt clairs.

<?php    
     /**
     * This returns all template variables needed for rendering
     * the product list, the facets, the pagination and the sort orders.
     *
     * @return array variables ready for templating
     */
    protected function getProductSearchVariables()
    {
        /*
         * To render the page we need to find something (a ProductSearchProviderInterface)
         * that knows how to query products.
         */
 
        // the search provider will need a context (language, shop...) to do its job
        $context = $this->getProductSearchContext();
 
        // the controller generates the query...
        $query = $this->getProductSearchQuery();
 
        // ...modules decide if they can handle it (first one that can is used)
        // Dans notre cas c'est ICI que le module devra être appellé
        $provider = $this->getProductSearchProviderFromModules($query);
 
        // if no module wants to do the query, then the core feature is used
        if (null === $provider) {
            $provider = $this->getDefaultProductSearchProvider();
        }
 
        $resultsPerPage = (int) Tools::getValue('resultsPerPage');
        if ($resultsPerPage <= 0 || $resultsPerPage > 36) {
            $resultsPerPage = Configuration::get('PS_PRODUCTS_PER_PAGE');
        }
 
        // we need to set a few parameters from back-end preferences
        $query
            ->setResultsPerPage($resultsPerPage)
            ->setPage(max((int) Tools::getValue('page'), 1))
        ;
 
        // set the sort order if provided in the URL
        if (($encodedSortOrder = Tools::getValue('order'))) {
            $query->setSortOrder(SortOrder::newFromString(
                $encodedSortOrder
            ));
        }
 
        // get the parameters containing the encoded facets from the URL
        $encodedFacets = Tools::getValue('q');
 
        /*
         * The controller is agnostic of facets.
         * It's up to the search module to use /define them.
         *
         * Facets are encoded in the "q" URL parameter, which is passed
         * to the search provider through the query's "$encodedFacets" property.
         */
 
        $query->setEncodedFacets($encodedFacets);
 
        // We're ready to run the actual query!
 
        $result = $provider->runQuery(
            $context,
            $query
        );
 
        // sort order is useful for template,
        // add it if undefined - it should be the same one
        // as for the query anyway
        if (!$result->getCurrentSortOrder()) {
            $result->setCurrentSortOrder($query->getSortOrder());
        }
 
        // prepare the products
        $products = $this->prepareMultipleProductsForTemplate(
            $result->getProducts()
        );
 
        // render the facets
        if ($provider instanceof FacetsRendererInterface) {
            // with the provider if it wants to
            $rendered_facets = $provider->renderFacets(
                $context,
                $result
            );
            $rendered_active_filters = $provider->renderActiveFilters(
                $context,
                $result
            );
        } else {
            // with the core
            $rendered_facets = $this->renderFacets(
                $result
            );
            $rendered_active_filters = $this->renderActiveFilters(
                $result
            );
        }
 
        $pagination = $this->getTemplateVarPagination(
            $query,
            $result
        );
 
        // prepare the sort orders
        // note that, again, the product controller is sort-orders
        // agnostic
        // a module can easily add specific sort orders that it needs
        // to support (e.g. sort by "energy efficiency")
        $sort_orders = $this->getTemplateVarSortOrders(
            $result->getAvailableSortOrders(),
            $query->getSortOrder()->toString()
        );
 
        $sort_selected = false;
        if (!empty($sort_orders)) {
            foreach ($sort_orders as $order) {
                if (isset($order['current']) && true === $order['current']) {
                    $sort_selected = $order['label'];
                    break;
                }
            }
        }
 
        $searchVariables = array(
            'label' => $this->getListingLabel(),
            'products' => $products,
            'sort_orders' => $sort_orders,
            'sort_selected' => $sort_selected,
            'pagination' => $pagination,
            'rendered_facets' => $rendered_facets,
            'rendered_active_filters' => $rendered_active_filters,
            'js_enabled' => $this->ajax,
            'current_url' => $this->updateQueryString(array(
                'q' => $result->getEncodedFacets(),
            )),
        );
 
        Hook::exec('filterProductSearch', array('searchVariables' => &$searchVariables));
        Hook::exec('actionProductSearchAfter', $searchVariables);
 
        return $searchVariables;
    }

En raccourci dans cette fonction notre module pourra renvoyer une classe de provider spécifique grâce à cet appel :

 $provider = $this->getProductSearchProviderFromModules($query);

Ensuite c’est la fonction

// We're ready to run the actual query!
 
        $result = $provider->runQuery(
            $context,
            $query
        );

qui va appeller la fonction runQuery du module en lui passant le contexte et la requête de recherche.

La suite de l’affichage ( facettes , tris, et pagination ) seront gérés automatiquement à partir des informations retournées par le provider de notre module.

 

Création du module et affichage de facettes

Maintenant que la base est comprise nous pouvons passer à la création du module, celui-ci s’appellera  hh_facetedSearch

Voici le code de base du module

require_once dirname(__FILE__).'/classes/Hh_facetedSearchProductSearchProvider.php';
class Hh_FacetedSearch extends Module
{
 
    public function __construct()
    {
 
        $this->author = 'hhennes';
        $this->name = 'hh_facetedsearch';
        $this->tab = 'test';
        $this->version = '0.1.0';
        $this->bootstrap = true;
        parent::__construct();
 
        $this->displayName = $this->l('HH Faceted Search');
        $this->description = $this->l('HH Sample Facets Implementation');
 
 
    }
 
    /**
     * Module installation.
     *
     * @return bool Success of the installation
     */
    public function install()
    {
        return parent::install()
            && $this->registerHook('productSearchProvider');
    }
 
    /**
     * Dans ce hook on intercepte la requête des nouveaux produits pour y ajouter des facettes
     * @param $params
     * @return Hh_facetedSearchProductSearchProvider
     */
    public function hookProductSearchProvider($params)
    {
        $query = $params['query'];
        if ($query->getQueryType() == 'new-products') {
            return new Hh_facetedSearchProductSearchProvider($this);
        }
    }
}

La point essentiel est qu’il doit être greffé sur le hook productSearchProvider et qu’il doit retourner une classe qui implémente l’interface PrestaShop\PrestaShop\Core\Product\Search\ProductSearchProviderInterface
Nous créerons le fichier dans classes/Hh_facetedSearchProductSearchProvider.php

Dans cette étape nous allons uniquement afficher des facettes ce qui est géré nativement par le controller prestashop.
Voici à présent le code du Product Search Provider, avec les commentaires explicatifs pour faire cela :

<?php 
//Use pour la recherche standard 
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchProviderInterface;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchContext; 
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery; 
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchResult; 
use PrestaShop\PrestaShop\Core\Product\Search\SortOrderFactory;
 //Use pour les facettes 
use PrestaShop\PrestaShop\Core\Product\Search\FacetCollection;#Collection de facettes 
use PrestaShop\PrestaShop\Core\Product\Search\Facet; #Classe de la facette 
use PrestaShop\PrestaShop\Core\Product\Search\Filter; #Classe des filtres 
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer; #Pour transformer l'url 
//Provider par défaut 
use PrestaShop\PrestaShop\Adapter\NewProducts\NewProductsProductSearchProvider; 
 
class Hh_facetedSearchProductSearchProvider implements ProductSearchProviderInterface { 
 
private $module; 
private $sortOrderFactory; 
 
/** 
 * Instanciation de la classe 
 * @param Hh_FacetedSearch $module 
 */ 
   public function __construct( Hh_FacetedSearch $module ) { $this->module = $module;
        //Récupération des tris disponibles par défaut
        $this->sortOrderFactory = new SortOrderFactory($this->module->getTranslator());
    }
 
    /**
     * @param ProductSearchContext $context
     * @param ProductSearchQuery $query
     * @return ProductSearchResult
     */
    public function runQuery(
        ProductSearchContext $context,
        ProductSearchQuery $query
    )
    {
 
        //Récupération des produits ( aucun changement par rapport au listing des nouveaux produits )
        if (!$products = $this->getProductsOrCount($context, $query, 'products')) {
            $products = array();
        }
        $count = $this->getProductsOrCount($context, $query, 'count');
 
        /**
         * Gestion du résulat
         * Envoi de la productSearchResult
         */
        $results = new ProductSearchResult();
 
        if (!empty($products)) {
 
            //Définition des résultats des produits
            $results
                ->setTotalProductsCount($count)
                ->setProducts($products);
 
            //Définition des tris disponibles ( Utilisation de ceux par défaut )
            $results->setAvailableSortOrders(
                $this->sortOrderFactory->getDefaultSortOrders()
            );
 
            //Récupération des filtres actifs , il sont situés dans l'url sous la forme q=filter-1|filtre-2 ect..
            $activeFilters = explode('|',$query->getEncodedFacets());
 
            //Définition des facettes disponibles ( c'est ici qu'on va définir nos facettes )
            $results->setFacetCollection(
                $this->getSampleFacets($activeFilters) //Appel de la fonction spécifique
            );
 
            //Définition des facettes actuellement utilisées
            $results->setEncodedFacets(
              $query->getEncodedFacets()
            );
 
        }
 
        return $results;
 
    }
 
    /**
     * Récupération des produits et du décompte
     * Dans cette partie on reprends le fonctionnement du controller des nouveaux produits
     * 
     * @param ProductSearchContext $context
     * @param ProductSearchQuery $query
     * @param type $type
     * @return type
     */
     private function getProductsOrCount(
        ProductSearchContext $context,
        ProductSearchQuery $query,
        $type = 'products'
    ) {
        return Product::getNewProducts(
            $context->getIdLang(),
            $query->getPage(),
            $query->getResultsPerPage(),
            $type !== 'products',
            $query->getSortOrder()->toLegacyOrderBy(),
            $query->getSortOrder()->toLegacyOrderWay()
        );
    }
 
    /**
     * Fonction d'explication sur comment afficher des facettes
     * @return FacetCollection
     */
    protected function getSampleFacets($activeFilters)
    {
 
        //Gestion des filtres actifs
        $activeFiltersQueryString ='';
        $activeFiltersQueryString .= implode('|',$activeFilters);
 
        //Création d'une collection de facettes
        $collection = new FacetCollection();
 
         //Création d'une facette
        $facet = new Facet();
        $facet->setLabel('Facette 1')
            ->setType('custom')
            ->setDisplayed(true) //Flag pour afficher ou nom la facette
            ->setWidgetType('checkbox') //Type de widget
            ->setMultipleSelectionAllowed(true); //Défini si on peut cocher plusieurs variantes
 
        //Ajout de filtres à cette facette
        $encodedFactetsUrl1 = $activeFiltersQueryString != '' ? $activeFiltersQueryString."|test-1": "test-1";
        $filter1 = new Filter();
        $filter1->setLabel('filtre 1') //Libellé du filtre
            ->setDisplayed(true) //Flag pour afficher ou nom le filtre
            ->setActive(in_array("test-1",$activeFilters) ? true : false ) //Définition si le filtre est actif ou non
            ->setType('test') // Type du filtre
            ->setValue('2') //Valeur du filtre
            ->setNextEncodedFacets($encodedFactetsUrl1) //Url pour afficher la filtre
            ->setMagnitude(1); //Nombre de résultats du filtre
 
        //Ajout du filtre à la facette
        $facet->addFilter($filter1);  
 
        $encodedFactetsUrl2 = $activeFiltersQueryString != '' ? $activeFiltersQueryString."|test-2": "test-2";
        //Idem pour un 2ème filtre
        $filter2 = new Filter();
        $filter2->setLabel('filtre 2') //Libellé du filtre
            ->setDisplayed(true) //Flag pour afficher ou nom le filtre
            ->setActive(in_array("test-2",$activeFilters) ? true : false ) //Définition si le filtre est actif ou non
            ->setType('test') // Type du filtre
            ->setValue('2') //Valeur du filtre
            ->setNextEncodedFacets($encodedFactetsUrl2) //Url pour afficher la filtre
            ->setMagnitude(3); //Nombre de résultats du filtre
 
        //Ajout du filtre à la facette
        $facet->addFilter($filter2);  
 
       //Ajout de la facette à la collection
        $collection->addFacet($facet);
 
        //Renvoi de la collection de facette
        return $collection;
 
    }
 
 
    /**
     * Provider de recherche par défaut
     * @return NewProductsProductSearchProvider
     */
    protected function getDefaultProductSearchProvider()
    {
        return new NewProductsProductSearchProvider(
            Context::getContext()->getTranslator()
        );
    }
 
}

La navigation à facettes est à présent bien visible sur la page des nouveaux produits.

Navigation facettes

La séléction / déselection est fonctionnelle lorsqu’on clique sur les différents éléments et tous les paramètres sont bien passés.
Notre navigation à facette est en place ( sans prise en compte des données )

Tri et gestion des facettes

Maintenant que la navigation à facette est fonctionnelle la prochaine étape et de renvoyer uniquement les facettes et les produits disponibles pour la sélection demandées.
Pour cette partie nous allons donc rajouter un filtre sur les catégories par défaut des produits.

Pour cela nous allons devoir rajouter 2 nouvelles fonction dans notre module :

  • getNewProducts => Reprends la fonction Product::getNewProducts et lui appliquera les filtres sélectionnés
  • getNewProductsCategoryFilters => Renverra la liste des filtres disponibles pour la séléection des produits

Voici le code complet du module fonctionnel qui permets de filtrer par les catégories par défaut du produit.
Fichier classes/Hh_facetedSearchProductSearchProvider.php

 

<?php
//Use pour la recherche standard
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchProviderInterface;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchContext;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchResult;
use PrestaShop\PrestaShop\Core\Product\Search\SortOrderFactory;
 
//Provider par défaut
use PrestaShop\PrestaShop\Adapter\NewProducts\NewProductsProductSearchProvider;
 
class Hh_facetedSearchProductSearchProvider implements ProductSearchProviderInterface
{
 
    private $module;
    private $sortOrderFactory;
 
    public function __construct(
        Hh_FacetedSearch $module
    ) {
        $this->module = $module;
        $this->sortOrderFactory = new SortOrderFactory($this->module->getTranslator());
    }
 
    /**
     * @param ProductSearchContext $context
     * @param ProductSearchQuery $query
     * @return ProductSearchResult
     */
    public function runQuery(
        ProductSearchContext $context,
        ProductSearchQuery $query
    )
    {
 
        //Récupération des filtres actifs
        $activeFilters = explode('|',$query->getEncodedFacets());
 
        //Récupération des résultats initiaux ( page des nouveaux produits )
        if (!$products = $this->getProductsOrCount($context, $query, 'products',$activeFilters)) {
            $products = array();
        }
        $count = $this->getProductsOrCount($context, $query, 'count',$activeFilters);
 
 
        //Récupération des filtres de catégories dispo pour la sélection de produits
        $categoryfilters = $this->module->getNewProductsCategoryFilters($products,$activeFilters);
 
        /**
         * Dernière Etape : Gestion du résulat
         * Envoi de la productSearchResult
         */
        $results = new ProductSearchResult();
 
        //Définition des résultats des produits
        $results
            ->setTotalProductsCount($count)
            ->setProducts($products);
 
        //Définition des tris disponibles
        $results->setAvailableSortOrders(
            $this->sortOrderFactory->getDefaultSortOrders()
        );
 
        //Définition des facettes disponibles
        if ( sizeof($categoryfilters->getFacets())){
            $results->setFacetCollection(
                $categoryfilters //C'est ici qu'on assigne les filtres de notre fonction
            );
        }
 
        //Définition des facettes utilisées
        $results->setEncodedFacets(
          $query->getEncodedFacets()
        );
 
        return $results;
 
    }
 
    private function getProductsOrCount(
        ProductSearchContext $context,
        ProductSearchQuery $query,
        $type = 'products',
        $activeFilter = array()
    ) {
        //La fonction appellée ici est celle du module
        return $this->module->getNewProducts(
            $context->getIdLang(),
            $query->getPage(),
            $query->getResultsPerPage(),
            $type !== 'products',
            $query->getSortOrder()->toLegacyOrderBy(),
            $query->getSortOrder()->toLegacyOrderWay(),
            Context::getContext(),
            $activeFilter //Rajout d'un paramètre pour passer les filtre sélectionnés
        );
    }
 
    /**
     * Provider de recherche par défaut
     * @return SearchProductSearchProvider
     */
    protected function getDefaultProductSearchProvider()
    {
        return new NewProductsProductSearchProvider(
            Context::getContext()->getTranslator()
        );
    }
 
}

Et le fichier du module. hh_facetedsearch.php

 

<?php
require_once dirname(__FILE__).'/classes/Hh_facetedSearchProductSearchProvider.php';
 
//Use pour les facettes
use PrestaShop\PrestaShop\Core\Product\Search\FacetCollection; #Collection de facettes
use PrestaShop\PrestaShop\Core\Product\Search\Facet; #Classe de la facette
use PrestaShop\PrestaShop\Core\Product\Search\Filter; #Classe des filtres
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer; #Pour transformer l'url

class Hh_FacetedSearch extends Module
{
 
    public function __construct()
    {
 
        $this->author = 'hhennes';
        $this->name = 'hh_facetedsearch';
        $this->tab = 'test';
        $this->version = '0.1.0';
        $this->bootstrap = true;
        parent::__construct();
 
        $this->displayName = $this->l('HH Faceted Search');
        $this->description = $this->l('HH Sample Facets Implementation');
 
 
    }
 
    /**
     * Module installation.
     *
     * @return bool Success of the installation
     */
    public function install()
    {
        return parent::install()
            && $this->registerHook('productSearchProvider');
    }
 
    /**
     * Dans ce hook on intercepte la requête des nouveaux produits pour y ajouter des facettes
     * @param $params
     * @return Hh_facetedSearchProductSearchProvider
     */
    public function hookProductSearchProvider($params)
    {
        //dump($this->context->controller);
        $query = $params['query'];
        if ($query->getQueryType() == 'new-products') {
            return new Hh_facetedSearchProductSearchProvider($this);
        }
    }
 
    /**
     * A partir des produits sélectionnés on déduit les filtres de catégories à afficher
     * @param array $activeFilters
     * @return FacetCollection
     */
    public function getNewProductsCategoryFilters($products , array $activeFilters)
    {
        //Récupération des catégories des produits et de leurs filtres
        $categoriesArray = [];
        foreach ( $products as $product )
        {
            if ( !array_key_exists($product['id_category_default'],$categoriesArray)){
                $categoriesArray[$product['id_category_default']] = 1;
            } else {
                $categoriesArray[$product['id_category_default']] = (int)$categoriesArray[$product['id_category_default']]+1;
            }
        }
 
        $activeFiltersQueryString ='';
        $activeFiltersQueryString .= implode('|',$activeFilters);
 
        //Création d'une collection de facettes
        $collection = new FacetCollection();
 
        if ( sizeof($categoriesArray)) {
 
            //Création d'une facette
            $facet = new Facet();
            $facet->setLabel($this->l('Catégories'))
                ->setType('category')
                ->setDisplayed(true)
                ->setWidgetType('checkbox')
                ->setMultipleSelectionAllowed(true);
 
            //Création des filtres avec les categories disponibles
            foreach ( $categoriesArray as $categoryId => $categoryCount) {
 
                $encodedFactetsUrl = $activeFiltersQueryString != '' ? $activeFiltersQueryString."|cat-".$categoryId : "cat-".$categoryId;
 
                $category = new Category($categoryId,$this->context->language->id);
                $filter = new Filter();
                $filter->setLabel($category->name)
                    ->setDisplayed(true)
                    ->setActive(in_array("cat-".$categoryId,$activeFilters) ? true : false )
                    ->setType('category')
                    ->setValue($categoryId)
                    ->setNextEncodedFacets($encodedFactetsUrl)
                    ->setMagnitude($categoryCount);
 
                //Ajout du filtre à la facette
                $facet->addFilter($filter);
            }
 
            //Ajout de la facette à la collection
            $collection->addFacet($facet);
        }
 
 
        return $collection;
    }
 
    /**
     * Get new products
     * ( Fonction modifiée de la classe produit avec gestion des filtres )
     *
     * @param int $id_lang Language id
     * @param int $pageNumber Start from (optional)
     * @param int $nbProducts Number of products to return (optional)
     * @return array New products
     */
    public function getNewProducts(
        $id_lang,
        $page_number = 0,
        $nb_products = 10,
        $count = false,
        $order_by = null,
        $order_way = null,
        Context $context = null ,
        $applieds_filters = array() //Nouveau paramètre de test
    )
    {
        $now = date('Y-m-d') . ' 00:00:00';
        if (!$context) {
            $context = Context::getContext();
        }
 
        $front = true;
        if (!in_array($context->controller->controller_type, array('front', 'modulefront'))) {
            $front = false;
        }
 
        if ($page_number < 1) {
            $page_number = 1;
        }
        if ($nb_products < 1) {
            $nb_products = 10;
        }
        if (empty($order_by) || $order_by == 'position') {
            $order_by = 'date_add';
        }
        if (empty($order_way)) {
            $order_way = 'DESC';
        }
        if ($order_by == 'id_product' || $order_by == 'price' || $order_by == 'date_add' || $order_by == 'date_upd') {
            $order_by_prefix = 'product_shop';
        } elseif ($order_by == 'name') {
            $order_by_prefix = 'pl';
        }
        if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way)) {
            die(Tools::displayError());
        }
 
        $sql_groups = '';
        if (Group::isFeatureActive()) {
            $groups = FrontController::getCurrentCustomerGroups();
            $sql_groups = ' AND EXISTS(SELECT 1 FROM `'._DB_PREFIX_.'category_product` cp
				JOIN `'._DB_PREFIX_.'category_group` cg ON (cp.id_category = cg.id_category AND cg.`id_group` '.(count($groups) ? 'IN ('.implode(',', $groups).')' : '= '.(int)Configuration::get('PS_UNIDENTIFIED_GROUP')).')
				WHERE cp.`id_product` = p.`id_product`)';
        }
 
        if (strpos($order_by, '.') > 0) {
            $order_by = explode('.', $order_by);
            $order_by_prefix = $order_by[0];
            $order_by = $order_by[1];
        }
 
        $nb_days_new_product = (int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT');
 
        if ($count) {
 
            //Gestion des conditions dans le count
            if ( sizeof($applieds_filters)) {
                $sqlFilters = "";
                foreach ($applieds_filters as $applieds_filter) {
 
                    if ( $applieds_filter == "" ) {
                        continue;
                    }
                    $catId = str_replace('cat-','',$applieds_filter);
                    if ( $catId == 0 ) {
                        continue;
                    }
                    $sqlFilters .= " INNER JOIN "._DB_PREFIX_."product ps_cat_".$catId.' ON  p.id_product = ps_cat_'.$catId.'.id_product AND ps_cat_'.$catId.'.id_category_default = '.$catId.' ';
                }
            }
 
            $sql = 'SELECT COUNT(p.`id_product`) AS nb
					FROM `'._DB_PREFIX_.'product` p 
                    '.$sqlFilters.'
					'.Shop::addSqlAssociation('product', 'p').'
					WHERE product_shop.`active` = 1
					AND product_shop.`date_add` > "'.date('Y-m-d', strtotime('-'.$nb_days_new_product.' DAY')).'"
					'.($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '').'
					'.$sql_groups;
 
            return (int)Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
        }
 
        $sql = new DbQuery();
        $sql->select(
            'p.*, product_shop.*, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`, pl.`meta_description`,
			pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`, image_shop.`id_image` id_image, il.`legend`, m.`name` AS manufacturer_name,
			(DATEDIFF(product_shop.`date_add`,
				DATE_SUB(
					"'.$now.'",
					INTERVAL '.$nb_days_new_product.' DAY
				)
			) > 0) as new'
        );
 
        $sql->from('product', 'p');
        $sql->join(Shop::addSqlAssociation('product', 'p'));
        $sql->leftJoin('product_lang', 'pl', '
			p.`id_product` = pl.`id_product`
			AND pl.`id_lang` = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('pl')
        );
        $sql->leftJoin('image_shop', 'image_shop', 'image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop='.(int)$context->shop->id);
        $sql->leftJoin('image_lang', 'il', 'image_shop.`id_image` = il.`id_image` AND il.`id_lang` = '.(int)$id_lang);
        $sql->leftJoin('manufacturer', 'm', 'm.`id_manufacturer` = p.`id_manufacturer`');
 
        //Gestion des filtres ( Pour l'exemple on ne gère que des catégories )
        if ( sizeof($applieds_filters)) {
            foreach ($applieds_filters as $applieds_filter) {
 
                if ( $applieds_filter == "") {
                    continue;
                }
 
                $catId = str_replace('cat-','',$applieds_filter);
                if ( $catId == 0 ) {
                    continue;
                }
                $sql->innerJoin('product','ps_cat_'.$catId,'p.id_product = ps_cat_'.$catId.'.id_product AND ps_cat_'.$catId.'.id_category_default = '.$catId);
            }
        }
 
        //Fin Gestion des filtres
 
        $sql->where('product_shop.`active` = 1');
        if ($front) {
            $sql->where('product_shop.`visibility` IN ("both", "catalog")');
        }
        $sql->where('product_shop.`date_add` > "'.date('Y-m-d', strtotime('-'.$nb_days_new_product.' DAY')).'"');
        if (Group::isFeatureActive()) {
            $groups = FrontController::getCurrentCustomerGroups();
            $sql->where('EXISTS(SELECT 1 FROM `'._DB_PREFIX_.'category_product` cp
				JOIN `'._DB_PREFIX_.'category_group` cg ON (cp.id_category = cg.id_category AND cg.`id_group` '.(count($groups) ? 'IN ('.implode(',', $groups).')' : '= 1').')
				WHERE cp.`id_product` = p.`id_product`)');
        }
 
        $sql->orderBy((isset($order_by_prefix) ? pSQL($order_by_prefix).'.' : '').'`'.pSQL($order_by).'` '.pSQL($order_way));
        $sql->limit($nb_products, (int)(($page_number-1) * $nb_products));
 
        if (Combination::isFeatureActive()) {
            $sql->select('product_attribute_shop.minimal_quantity AS product_attribute_minimal_quantity, IFNULL(product_attribute_shop.id_product_attribute,0) id_product_attribute');
            $sql->leftJoin('product_attribute_shop', 'product_attribute_shop', 'p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop='.(int)$context->shop->id);
        }
        $sql->join(Product::sqlStock('p', 0));
 
        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
 
        if (!$result) {
            return false;
        }
 
        if ($order_by == 'price') {
            Tools::orderbyPrice($result, $order_way);
        }
        $products_ids = array();
        foreach ($result as $row) {
            $products_ids[] = $row['id_product'];
        }
        // Thus you can avoid one query per product, because there will be only one query for all the products of the cart
        Product::cacheFrontFeatures($products_ids, $id_lang);
        return Product::getProductsProperties((int)$id_lang, $result);
    }
 
}

Une fois tout cela en place vous devriez avoir une navigation à facette sur les catégories des produits en place comme sur la capture ci-dessous.
Ce principe est applicable à toutes les pages prestashop étendant les listings.
Et il est également possible de créer des pages de listings personnalisées ( j’y reviendrais peut être dans un article ultérieurement )

Navigation à facettes prestashop

 

 

 

 
Pas de commentaires

Magento : Créer une commande via l’api

J’ai récemment eut à créer des commandes Magento via l’api et j’ai rencontré pas mal de difficultés, j’en profite pour condenser l’ensemble du processus dans cet article 🙂

Voici un script qui permettra de créer une commande Magento à partir de l’api Soap de Magento ( à la fois avec la version api V1 et api V2 )
Cet exemple a été réalisé sur un Magento 1.9.3.x de démonstration utilisant les samples data Magento.

N’hésitez pas à consulter la documentation officielle des api, si des points ne sont pas assez clairs  : https://devdocs.magento.com/guides/m1x/api/soap/introduction.html

Le script part des postulats suivants :

  • Passage de commande pour un client existant
  • Avec une adresse de facturation par défaut de renseignée
  • Les adresses de livraison et de facturation seront similaires.
  • Un accès à l’api avec tous les accès nécessaires existe sur le site magento de destination.

Les actions effectuées par le script sont les suivantes :

  1. Login à l’api
  2. Récupération de l’identifiant du  client à partir de son email
  3. Récupération de l’identifiant de l’adresse de facturation/livraison du client
  4. Création d’un panier
  5. Assignation du client au panier
  6. Assignation des adresses du client au panier
  7. Ajout de produits au panier
    1. Produit simple
    2. Produit configurable
    3. Produit groupé
    4. Produit Bundle
  8. Récupération des modes de livraison disponibles pour le panier
  9. Assignation du mode de livraison du panier
  10. Récupération des modes de paiement disponibles pour le panier
  11. Assignation du mode de paiement du panier
  12. Transformation panier en commande

 

Voir le script complet pour l’api V1
Voir le script complet pour l’api V2

Avant de vous montrer le script complet voici un focus sur les éléments à comprendre

Ajout des produits au panier

En fonction du type de produit à ajouter au panier , les informations à envoyer ne seront pas les mêmes.
Nous allons donc voir en détails comment ajouter les produits et récupérer les informations pour le faire

Produit simple :
L’ajout est rapide peut se faire en ciblant le produit via son identifiant Magento ou son SKU
Ceci fonctionne également pour les produits virtuels et téléchargeables.

 //Ajout d'un produit simple ( fonctionne aussi pour virtuel et téléchargeable )
	$simpleProductData = array(
		'sku' => 'acj005', //Sku du produit simple
                //'product_id' => 10 , Identifiant du produit en alternative
		'qty' => 2, //Qté du produit simple
	);

Produit configurable :
En complément des informations du produit simple, il est nécessaire d’implémenter le tableau « super_attribute » pour que l’ajout se fasse correctement.
Les valeurs qui doivent être renseignées sont sous la forme
AttributeID => OptionID

Attention à bien renseigner tous les attributs obligatoires pour que l’ajout fonctionne.

 //Ajout d'un produit configurable (ex sur démo magento bowery-chino-pants-546.html )
	$configurableProductData = array(
		'sku' => 'mpd003c' ,//Sku produit 
		'qty' => 1, //Qté du produit 
		'super_attribute' => array( //Champ spécifique au produit configurable
			92 => 17, //sous la forme attributeID => optionID
			180 => 67,
		),
	);

Il est possible de trouver ces informations directement sur le site en inspectant la source avec votre navigateur :

Les selects contiennent l’identifiant du super attribute dans leur nom, et la valeur est stockée dans l’option.

 

Produit Groupé :
En complément des informations du produit simple, il est nécessaire d’implémenter le tableau « super_group » pour que l’ajout se fasse correctement.
Les valeurs qui doivent être renseignées sont sous la forme
SuperGroupId => Qty

Attention à bien renseigner tous les attributs obligatoires pour que l’ajout fonctionne.

 //Ajout d'un produit groupé : pearl-necklace-set-test.html
	$groupedProductData = array(
		'sku' => 'acj007',
		'qty' => 1,
		'super_group' => array(
			547 => 1, //SuperGroupId => Qty
			548 => 2,
			551 => 1,	
		)
	);

Il est possible de trouver ces informations directement sur le site en inspectant la source avec votre navigateur :

Magento api grouped

Produit Bundle :
En complément des informations du produit simple, il est nécessaire d’implémenter les tableaux « bundle_option » et « bundle_qty » pour que l’ajout se fasse correctement.

 //Ajout d'un produit bundle mp3-player-with-audio.html
	$bundleProductData = array(
	  'product_id' => 446,
          'qty' => 1, //Quantité à 1 pour le bundle	  
	  'bundle_option' => array( 
	  21 => 84, //Quantité des options du panier sous la forme OptionID => SelectionID
	  22 => 86 ), 
	  'bundle_quantity' => array(
	  84 => 2, //Quantité de chaque option ajoutée au panier sous la forme SelectionID => Qté
	  86 => 2 
	  ), 
	);

Il est possible de trouver ces informations directement sur le site en inspectant la source avec votre navigateur :

Magento api bundle

 

Récupération des modes de paiement et de livraison

En fonction des paramètres de votre commande et selon la configuration de votre site, les modes de paiement et de livraison disponibles peuvent être amenés à changer.
C’est pourquoi je recommande l’utilisation des méthodes de l’api qui permettent de voir les modes disponibles avant de les assigner, les appels à faire étant les suivants :

//Récupération des modes de livraison disponibles pour la commande
$shippingMethods = $client->call($session, ‘cart_shipping.list, $shoppingCartId);
var_dump($shippingMethods);
//Récupération des modes de paiement disponibles pour la commande
$paymentMethods = $client->call($session, ‘cart_payment.list, $shoppingCartId);
var_dump($paymentMethods);

 

Pour finir voici donc le script complet pour l’API V1

 

<?php
error_reporting(E_ALL);
ini_set('display_errors','on');
echo '<pre>';
 
$adr = "http://magento-1933.demo/api/?wsdl";
$client = new SoapClient($adr,array('trace' => 1));
$apiUser = "testuser";
$apiKey = "test2018";
 
/**
* Création d'une commande Magento
* Via l'api Soap V1
*/
try {
 
//login à magento
$session = $client->login($apiUser, $apiKey);
 
//Récupération d'un client à partir de son email
$customerFilers = array(
'email' => array('eq' => '[email protected]')
);
$customersList = $client->call($session,'customer.list',array($customerFilers));
 
if ( $customersList ) {
$customerID = $customersList[0]['customer_id'];
} else {
exit('Erreur : Client non existant');
}
 
//Récupération de l'adresse de facturation par défaut du client
$addressList = $client->call($session, 'customer_address.list',$customerID);
 
if ( $addressList ) {
$addressId = false;
foreach ( $addressList as $address ) {
if ( $address['is_default_billing'] === true ){
$addressId = $address['customer_address_id'];
break;
}
}
} else {
exit("Erreur : Pas d'adresses pour ce client");
}
 
if ( !$addressId ) {
exit("Erreur : Pas d'adresse par défaut pour ce client");
}
 
//Création d'un panier ( initial )
$shoppingCartId= $client->call( $session, 'cart.create', array(1) );
 
//Assignation du client au panier
$assignCustomer = $client->call($session,'cart_customer.set',array($shoppingCartId,
array(
'mode' => 'customer',
'customer_id' => $customerID
)
)
);
//Assignation de l'adresse du client au panier , possibilité de définir une adresse de facturation et de livraison différente
$Adresses = $client->call($session, 'cart_customer.addresses',
array( $shoppingCartId ,
array(
array('mode' => 'billing','address_id' => $addressId),
array('mode' => 'shipping','address_id' => $addressId )
),
)
);
 
//Ajout d'un produit simple ( fonctionne aussi pour virtuel et téléchargeable )
$simpleProductData = array(
'sku' => 'acj005', //Sku du produit simple
'qty' => 2, //Qté du produit simple
);
 
//Ajout d'un produit configurable (ex sur démo magento bowery-chino-pants-546.html )
$configurableProductData = array(
'sku' => 'mpd003c' ,//Sku produit
'qty' => 1, //Qté du produit
'super_attribute' => array( //Champ spécifique au produit configurable
92 => 17, //sous la forme attributeID => optionID
180 => 67,
),
);
 
//Ajout d'un produit groupé
$groupedProductData = array(
'sku' => 'acj007',
'qty' => 1,
'super_group' => array(
547 => 1, //SuperGroupId => Qty
548 => 2,
551 => 1,
)
);
 
//Ajout d'un produit bundle mp3-player-with-audio.html
$bundleProductData = array(
'product_id' => 446,
'qty' => 1, //Quantité à 1 pour le bundle
'bundle_option' => array(
21 => 84,
22 => 86 ), //Quantité des options du panier sous la forme OptionID => SelectionID
'bundle_quantity' => array(
84 => 2,
86 => 2
), //Quantité de chaque option ajoutée au panier sous la forme SelectionID => Qté*/
);
 
//Assignation de produits au panier
$addProductToCart = $client->call($session,'cart_product.add',
array(
'quote_id' => $shoppingCartId,
array(
$simpleProductData,
$configurableProductData,
$groupedProductData,
$bundleProductData
),
)
);
 
//Récupération des modes de livraison disponibles pour la commande
$shippingMethods = $client->call($session, 'cart_shipping.list', $shoppingCartId);
 
//Décommenter la ligne suivante pour voir les méthodes de livraison disponibles
//var_dump($shippingMethods);
 
//Assignation d'un mode de livraison ( par défault on prends le freeshipping )
$assignShippingMethod = $client->call($session, 'cart_shipping.method', array($shoppingCartId,'freeshipping_freeshipping'));
 
//Récupération des modes de paiement disponibles pour la commande
$paymentMethods = $client->call($session, 'cart_payment.list', $shoppingCartId);
 
//Décommenter la ligne suivante pour voir les méthodes de paiement disponibles
//var_dump($paymentMethods);
 
//Assignation d'un mode de paiement : par défaut cashondelivery
$assignPaymenMethod = $client->call($session, 'cart_payment.method', array($shoppingCartId,array( 'method' => 'cashondelivery')));
 
//Transformation du paiement en panier
$transformOrder = $client->call($session, 'cart.order', array($shoppingCartId));
 
if ( $transformOrder ) {
echo 'Création de la commande '.$transformOrder.'<br />';
}
 
} catch ( Exception $e ) {
 
echo $e->getMessage();
 
}

Et le script pour l’API V2

 

<?php
error_reporting(E_ALL);
ini_set('display_errors','on');
echo '<pre>';
 
//API V2
$adr = "http://magento-1933.demo/api/?wsdl";
$client = new SoapClient($adr,array('trace' => 1));
$apiUser = "testuser";
$apiKey = "test2018";
 
/**
* Création d'une commande Magento
* Via l'api Soap V2
*/
try {
 
//login à magento
$session = $client->login($apiUser, $apiKey);
 
//Création d'un panier ( initial )
$shoppingCartId= $client->shoppingCartCreate( $session,1);
 
//Récupération d'un client à partir de son email
$customerFilers = array(
'email' => array('eq' => '[email protected]')
);
$customersList = $client->customerCustomerList($session,array($customerFilers));
 
if ( $customersList ) {
$customerID = $customersList[0]->customer_id;
} else {
exit('Erreur : Client non existant');
}
 
//Assignation d'un client au panier
$assignCustomer = $client->shoppingCartCustomerSet($session,$shoppingCartId,
array(
'mode' => 'customer',
'customer_id' => $customerID
)
);
 
//Récupération de l'adresse de facturation par défaut du client
$addressList = $client->customerAddressList($session,$customerID);
 
if ( $addressList ) {
$addressId = false;
foreach ( $addressList as $address ) {
if ( $address->is_default_billing === true ){
$addressId = $address->customer_address_id;
break;
}
}
} else {
exit("Erreur : Pas d'adresses pour ce client");
}
 
if ( !$addressId ) {
exit("Erreur : Pas d'adresse par défaut pour ce client");
}
 
//Assignation de cette adresse au panier
$Adresses = $client->shoppingCartCustomerAddresses($session, $shoppingCartId ,
array(
array('mode' => 'billing','address_id' => $addressId),
array('mode' => 'shipping','address_id' => $addressId )
)
);
 
//Ajout d'un produit simple ( fonctionne aussi pour virtuel et téléchargeable )
$simpleProductData = array(
'sku' => 'acj005', //Sku du produit simple
'qty' => 2, //Qté du produit simple
);
 
//Ajout d'un produit configurable (ex sur démo magento bowery-chino-pants-546.html )
$configurableProductData = array(
'sku' => 'mpd003c' ,//Sku produit
'qty' => 1, //Qté du produit
'super_attribute' => array( //Champ spécifique au produit configurable
92 => 17, //sous la forme attributeID => optionID
180 => 67,
),
);
 
//Ajout d'un produit groupé
$groupedProductData = array(
'sku' => 'acj007',
'qty' => 1,
'super_group' => array(
547 => 1, //SuperGroupId => Qty
548 => 2,
551 => 1,
)
);
 
//Ajout d'un produit bundle mp3-player-with-audio.html
$bundleProductData = array(
'product_id' => 446,
'qty' => 1, //Quantité à 1 pour le bundle
'bundle_option' => array(
21 => 84,
22 => 86 ), //Quantité des options du panier sous la forme OptionID => SelectionID
'bundle_quantity' => array(
84 => 2,
86 => 2
), //Quantité de chaque option ajoutée au panier sous la forme SelectionID => Qté*/
);
 
//Assignation de produits au panier
$addProductToCart = $client->shoppingCartProductAdd($session,$shoppingCartId,
array(
$simpleProductData,
$configurableProductData,
$groupedProductData,
$bundleProductData
)
);
 
//Récupération des modes de livraison disponibles pour la commande
$shippingMethods = $client->shoppingCartShippingList ($session,$shoppingCartId);
 
//Décommenter la ligne suivante pour voir les méthodes de livraison disponibles
//var_dump($shippingMethods);
 
//Assignation d'un mode de livraison ( par défault on prends le freeshipping )
$assignShippingMethod = $client->shoppingCartShippingMethod($session,$shoppingCartId,'freeshipping_freeshipping');
 
//Récupération des modes de paiement disponibles pour la commande
$paymentMethods = $client->shoppingCartPaymentList($session,$shoppingCartId);
 
//Décommenter la ligne suivante pour voir les méthodes de paiement disponibles
//var_dump($paymentMethods);
 
//Assignation d'un mode de paiement : par défaut cashondelivery
$assignPaymenMethod = $client->shoppingCartPaymentMethod($session,$shoppingCartId,array( 'method' => 'cashondelivery'));
 
//Transformation du paiement en panier
$transformOrder = $client->shoppingCartOrder($session,$shoppingCartId);
 
if ( $transformOrder ) {
echo 'Création de la commande '.$transformOrder.'<br />';
}
 
} catch ( Exception $e ) {
echo $e->getMessage();
}
Pas de commentaires

Prestashop : Ajouter un objet dans l’api

Nous allons voir comment ajouter un nouvel objet personnalisé dans l’api Prestashop via un module.
Cette méthodologie fonctionne sous Prestashop 1.6 et 1.7

L’objectif est d’ajouter de rendre un nouvel objet « Sample » qui pourra être manipulé via l’api.
Pour cela nous allons créer un module hhapisample

Ce module va ajouter une entité « sample » qui sera gérable via l’API.

Cette entité aura uniquement les paramètres suivants :

  • référence
  • nom ( multilingue )
  • description ( multilingue )

Voici le code de cette entité à placer dans le fichier classes/Sample.php du module.

/**
 * Classe d'exemple pour le webservice
 */
class Sample extends ObjectModel {
 
    /** @var string Référence du document */
    public $reference;
 
    /** @var string nom */
    public $name;
 
    /** @var string description */
    public $description;
 
    /**
     * Définition des paramètres de la classe
     */
    public static $definition = array(
        'table' => 'sample',
        'primary' => 'id_sample',
        'multilang' => true,
        'multilang_shop' => false,
        'fields' => array(
            'reference' => array('type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'size' => 255),
            'name' => array('type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'size' => 255 , 'lang' => true),
            'description' => array('type' => self::TYPE_STRING, 'validate' => 'isCleanHtml' , 'lang' => true),
        ),
    );
 
    /**
     * Mapping de la classe avec le webservice
     * 
     * @var type 
     */
    protected $webserviceParameters = [
        'objectsNodeName' => 'samples', //objectsNodeName doit être la valeur déclarée dans le hookAddWebserviceResources ( liste des entités )
        'objectNodeName' => 'sample', // Détail d'une entité
        'fields' => []
    ];
}

La définition du webservice est géré dans l’objet est configuré dans la variable $webserviceParameters
Pour rendre disponible cette entité, il faut greffer votre module sur le hook addWebserviceResources

/**
     * Ajout de la nouvelle entité au webservice
     * @param $params
     * @return array
     */
    public function hookAddWebserviceResources($params) {
        return [
            'samples' => [ //Nom du paramètre $webserviceParameters['objectsNodeName'] de la classe Objet
                'description' => 'Sample new entity for API',
                'class' => 'Sample'
            ],
        ];
    }

Une fois le module installé, vous pouvez-voir dans la liste des objets de l’api que l’entité sample est bien visible.

En vous rendant dans « Paramètres avancés / Webservice »
Il faut autoriser les utilisateurs api souhaités à accéder à l’entité pour pouvoir la modifier.
Api prestashop

Lors de l’appel à la page de l’api avec un utilisateur qui a les droits nécessaire nous pourrons voir les informations suivantes :

Api Prestashop sample

Notre nouvel objet est donc bien disponible via l’api et il est possible de le géré comme tous les autres 🙂

Voici le code complet du module

//Inclusion de la classe du nouveau modèle
include_once dirname(__FILE__) . '/classes/Sample.php';
 
/**
 *
 * Sample module to add custom object API
 */
class HhApiSample extends Module {
 
    public function __construct() {
        $this->name = 'hhapisample';
        $this->tab = 'others';
        $this->version = '0.1.0';
        $this->author = 'hhennes';
        $this->need_instance = 0;
        $this->bootstrap = true;
 
        parent::__construct();
 
        $this->displayName = $this->l('HH api Sample');
        $this->description = $this->l('Sample module to add custom entity API');
    }
 
    /**
     * Installation du module
     * @return bool
     */
    public function install() {
 
        if (!parent::install() 
            || !$this->registerHook('addWebserviceResources') 
            || !$this->installSql()
        ) {
            return false;
        }
 
        return true;
    }
 
    /**
     * Désinstallation du module
     * @return bool
     */
    public function uninstall() {
        if (!parent::uninstall() || !$this->uninstallSql()
        ) {
            return false;
        }
 
        return true;
    }
 
    /**
     * Install Sql
     * @return bool
     */
    protected function installSql() {
        //Création de la table de l'entité "Sample"
        $sql = "CREATE TABLE `" . _DB_PREFIX_ . "sample` (
                  `id_sample` int(11) unsigned NOT NULL AUTO_INCREMENT,
                  `reference` int(11) DEFAULT NULL,
                  PRIMARY KEY (`id_sample`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8;";
 
        //Création de la table de langue de l'entité "Sample"
        $sqlLang = "CREATE TABLE `" . _DB_PREFIX_ . "sample_lang` (
                  `id_sample` int(11) unsigned NOT NULL AUTO_INCREMENT,
                  `id_lang` int(11) unsigned NOT NULL,
                  `name` varchar(255) DEFAULT NULL,
                  `description` TEXT DEFAULT NULL,
                  PRIMARY KEY (`id_sample`,`id_lang`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8;";
 
        return Db::getInstance()->execute($sql) && Db::getInstance()->execute($sqlLang);
    }
 
    /**
     * Uninstall Sql
     * @return bool
     */
    protected function unInstallSql() {
        $sql = "DROP TABLE IF EXISTS " . _DB_PREFIX_ . "sample;"
                . "DROP TABLE IF EXISTS " . _DB_PREFIX_ . "sample_lang;";
        return Db::getInstance()->execute($sql);
    }
 
    /**
     * Ajout de la nouvelle entité au webservice
     * @Todo Vérifier les versions compatibles
     * @param $params
     * @return array
     */
    public function hookAddWebserviceResources($params) {
        return [
            'samples' => [
                'description' => 'Sample new entity for API',
                'class' => 'Sample'
            ],
        ];
    }
 
}
Pas de commentaires

Prestashop 1.7 : Utilisation des formfields

La version 1.7 rajoute une nouvelle notion pour gérer les champs sur certains formulaires front ( Client et Adresse entre autre )
Ceux-ci sont géré via la classe FormField et leur affichage est géré dans le template themes/themeName/templates/_partials/form-fields.tpl

Les méthodes utiles pour gérer ces champs sont les suivantes :

  • setName: Défini le nom du champ
  • setType: Défini le type du champ ( text/checkbox .. ) voir liste ci-dessous
  • setRequired : Défini si le champ est obligatoire ou non
  • setLabel : Défini le label du champ
  • setValue : Défini la valeur du champ
  • setAvailableValues : Défini les valeurs disponibles pour le champ, pour les select entre autre
  • addAvailableValue : Ajout une valeur disponible pour le champ
  • setMaxLength: Longueur maximum du champ
  • setConstraint : Ajoute une contrainte de validation au champ , les méthodes appelables sont celles de la classe Validation ( ex isEmail )

Pour l’exemple j’ai ajouté des nouveaux champs au formulaire de création de compte client via le hook hookAdditionalCustomerFormFields

Voici l’ensemble des  types de champs possibles :

return [
    //Champ texte standard
    (new FormField)
        ->setName('professionnal_id')
        ->setType('text')
        ->setRequired(true) //Décommenter pour rendre obligatoire
        ->setValue("TEST")
        ->setMaxLength("10")
        ->setLabel($this->l('Professionnal id')),
    //Champ File
    (new FormField)
        ->setName('justificatif_upload')
        ->setType('file')
        ->setLabel($this->l('document ID')),
    //Select
    (new FormField)
        ->setName('select_field')
        ->setType('select')
        ->setAvailableValues(array('key' => 'value 1', 'key2' => 'value2'))
        ->setLabel($this->l('Select type')),
    //countrySelect ( idem select mais rajoute une classe js js-country
    (new FormField)
        ->setName('country_field')
        ->setType('countrySelect')
        ->setAvailableValues(['key' => 'value 1', 'key2' => 'value2'])
        ->setLabel($this->l('Country Select')),
    //Checkbox
    (new FormField)
        ->setName('checkbox_field[]')
        ->setType('checkbox')
        ->setValue(1)
        ->setLabel($this->l('Checkbox type')),
    //radio-buttons
    (new FormField)
        ->setName('radio_field')
        ->setType('radio-buttons')
        ->setAvailableValues(array('key' => 'value 1', 'key2' => 'value2'))
        ->setLabel($this->l('Radio buttons type')),
    //date
    (new FormField)
        ->setName('date_field')
        ->setType('date')
        ->setLabel($this->l('Date')),
    //birthday
    (new FormField)
        ->setName('birthday_field')
        ->setType('birthday')
        ->setLabel($this->l('birthday')),
    //password
    (new FormField)
        ->setName('password_field')
        ->setType('password_')
        ->setLabel($this->l('password_')),
    //Champ email
    (new FormField)
        ->setName('email_field')
        ->setType('email')
        ->setLabel($this->l('email type')),
    //Champ tel
    (new FormField)
        ->setName('phone_field')
        ->setType('phone')
        ->setLabel($this->l('Phone type')),
    //Champ caché
    (new FormField)
        ->setName('hidden_field')
        ->setType('hidden')
        ->setValue('My hidden value')
];

Et leur visualisation :

Form fields prestashop

Pas de commentaires

Prestashop 1.7 : Récupérer des informations personnalisées dans le panier

J’ai récemment fait face à un problème, car je souhaitais remontée un champ produit spécifique dans les informations du panier sur prestashop 1.7
( La modification décrite fonctionne également sur prestashop 1.6.1.x )

Après analyse, il s’avère que les informations disponibles sont remontées depuis la fonction getProducts de la classe Cart.

L’ensemble des champs remontés sont listé dans la requête sql de remontée des produits, et il n’est pas possible de récupérer nos nouveaux champs.

// Build query
$sql = new DbQuery();
 
// Build SELECT
$sql->select('cp.`id_product_attribute`, cp.`id_product`, cp.`quantity` AS cart_quantity, cp.id_shop, cp.`id_customization`, pl.`name`, p.`is_virtual`,
pl.`description_short`, pl.`available_now`, pl.`available_later`, product_shop.`id_category_default`, p.`id_supplier`,
p.`id_manufacturer`, m.`name` AS manufacturer_name, product_shop.`on_sale`, product_shop.`ecotax`, product_shop.`additional_shipping_cost`,
product_shop.`available_for_order`, product_shop.`show_price`, product_shop.`price`, product_shop.`active`, product_shop.`unity`, product_shop.`unit_price_ratio`,
stock.`quantity` AS quantity_available, p.`width`, p.`height`, p.`depth`, stock.`out_of_stock`, p.`weight`,
p.`available_date`, p.`date_add`, p.`date_upd`, IFNULL(stock.quantity, 0) as quantity, pl.`link_rewrite`, cl.`link_rewrite` AS category,
CONCAT(LPAD(cp.`id_product`, 10, 0), LPAD(IFNULL(cp.`id_product_attribute`, 0), 10, 0), IFNULL(cp.`id_address_delivery`, 0), IFNULL(cp.`id_customization`, 0)) AS unique_id, cp.id_address_delivery,
product_shop.advanced_stock_management, ps.product_supplier_reference supplier_reference');
 
// Build FROM
$sql->from('cart_product', 'cp');
 
...

Vous pouvez consulter le fichier complet ici : https://github.com/PrestaShop/PrestaShop/blob/1.7.3.x/classes/Cart.php#605

La bonne nouvelle c’est que la requête est effectuée via un objet Dbquery, ce qui va nous permettre d’interférer assez facilement avec la requête.

Pour ajouter une fonctionnalité de modification de cette requête nous allons donc créer une surcharge de la classe Cart qui va reprendre la function getProducts de la classe parente et rajouter le contenu suivant :

// Nouveau hook pour pouvoir modifier la requête de récupération des produits
Hook::exec('actionCartGetProductsSqlQuery',['query' => &$sql]);

juste avant l’exécution de la requête dans la ligne

$result = Db::getInstance()->executeS($sql);

Via ce hook nouvellement créé auquel l’instance de l’objet DqQuery est passé en paramètre, il est à présent possible de faire un peu prêt tout sur cette requête.
La variable étant passée par référence, les modifications sur la requête sont appliquées directement.

Pour ajouter un champ il suffira donc de créer un module et de le greffer sur le hook actionCartGetProductsSqlQuery

/**
 * Nouveau Hook pour modifier la requête de sélection des produits du panier
 * Cf. override du cart
 * @param array $params => 'query' => &DbQuery
 */
public function hookActionCartGetProductsSqlQuery($params)
{
    $params['query']->select('p.marque');
}
Pas de commentaires

Prestashop : Modifier les listings dans l’administration

En complément d’un de mes précédents articles sur comment ajouter des nouveaux champs dans le listing des produits ( pour prestashop 1.7 ), nous allons voir comment effectuer cette action sur les autres controllers de l’administration.

Les listing des controllers concernés sont ceux qui utilisent encore l’ancienne infrastructure et les anciennes méthodes ( Clients, adresses, commandes , employés … )

Ce tutoriel fonctionne à partir de la version 1.6.0.2 de Prestashop et utilise le hook dynamique action.$this->controller_name.ListingFieldsModifier

Pour les versions inférieures il sera nécessaire de faire un override du controller concerné et d’ajouter les informations directement à la suite des paramètres de classe
$this->_select,
$this->_join,
$this->_where

Exécuté par via le code suivant dans la fonction getList de la classe AdminController

Hook::exec('action'.$this->controller_name.'ListingFieldsModifier', array(
'select' => &$this->_select,
'join' => &$this->_join,
'where' => &$this->_where,
'group_by' => &$this->_group,
'order_by' => &$this->_orderBy,
'order_way' => &$this->_orderWay,
'fields' => &$this->fields_list,
));

Les paramètres sont passés par référence au hook ce qui permets de modifier directement les variables qui seront appellées depuis un module.

Les éléments select, join, where, group_by, order_by , order_way permettent de modifier la requête de récupération de la liste des entités.
Alors que le champ fields est un tableau qui regroupe l’ensemble des colonnes qui seront affichées dans la liste.

Pour tester tout cela nous allons faire un module hh_admin dont le but sera le suivant :

– Ajouter 2 nouvelles colonnes dans le listing des clients et supprimer 2 existants :

Le module devra donc implémenter le hook suivant :

  • actionAdminCustomersListingFieldsModifier

Pour modifier le listing client le contenu sera le suivant :

    /**
     * Ajout de champs dans le listing des clients
     * @param type $params
     */
    public function hookActionAdminCustomersListingFieldsModifier($params)
    {
        //Pour l'exemple on va ajouter 2 champs dans le listing
 
        // champ simple texte : date d'inscription à la newsletter ( newsletter_date_add dans la table customer )
        $params['fields']['newsletter_date_add'] = array(
            'title' => $this->l('Date souscription Newsletter')
        );
 
        //Champ un peu plus "complexe" groupe par défaut  select ( id_default_group dans la table customer et récupération des noms des groupes via une jointure )
        $params['select'] .= ' ,grl.name as default_group_name '; //Ajout du champ dans la requête select
        $params['join'] .= 'LEFT JOIN '._DB_PREFIX_.'group_lang grl ON (a.id_default_group = grl.id_group )'; //Jointure avec la table des groupes
 
	//Récupération de la liste des groupes via la méthode prestashop + création d'un tableau sous la forme id_group => name 
	$groups = Group::getGroups($this->context->language->id); 
        $groupsList = array();
        foreach( $groups as $group)
            $groupsList[$group['id_group']] = $group['name'];
 
        //Ajout du nouveau champ à afficher dans le listing
        $params['fields']['default_group_name'] = array(
            'title' => $this->l('Groupe par défaut'),
            'type' => 'select',
            'list' => $groupsList,
            'filter_key' => 'a!id_default_group' //Clé de filtrage dans la requête sous la forme "NomTable!champ"  
        );
 
	//Suppression des champs "Titre","Optin"
        unset($params['fields']['title']);
        unset($params['fields']['optin']);
    }

Une fois le code implémenté nous voyons bien nos 2 nouvelles colonnes dans la liste et les 2 anciennes ont bien disparu.
Tout est fonctionnel sans la moindre surcharge 🙂



Bonus :
Une demande récurrente est également de pouvoir placer les nouveaux champs à un autre endroit qu’a la fin.

Pour cela j’ai créé la fonction suivante :

    /**
     * Ajout du nouveau champ après un autre 
     * @param type $fieldParams
     * @param type $afertName
     * @param type $fieldsArray
     */
    public function addNewFieldAfter($fieldParams,$afterName,array &$fieldsArray)
    {
 
        if (array_key_exists($afterName, $fieldsArray)) {
            $pos = array_search($afterName, array_keys($fieldsArray));
            $before = array_slice($fieldsArray,0,$pos+1);
            $after = array_slice($fieldsArray, $pos); 
            $fieldsArray = array_merge($before,$fieldParams,$after);
 
        } else {
            $fieldsArray = array_merge($fieldsArray,$fieldParams);
        }
    }

Il est possible de l’utiliser de cette manière :

 /**
     * Ajout de champs dans le listing des clients
     * @param type $params
     */
    public function hookActionAdminCustomersListingFieldsModifier($params)
    {
 
        $newField = array( 'default_group_name' => 
		array(
            'title' => $this->l('Groupe par défaut'),
            'type' => 'select',
            'list' => $groupsList,
            'filter_key' => 'a!id_default_group' //Clé de filtrage dans la requête sous la forme "NomTable!champ" 
            )
        );
 
        $this->addNewFieldAfter($newField, 'lastname', $params['fields']);
 
    }

Voici le code code complet du module :

<?php
class HhAdmin extends Module {
 
    public function __construct() {
        $this->name = 'hhadmin';
        $this->tab = 'others';
        $this->author = 'hhennes';
        $this->version = '0.1.0';
        $this->need_instance = 0;
        $this->bootstrap = true;
 
        parent::__construct();
 
        $this->displayName = $this->l('hhadmin');
        $this->description = $this->l('hhadmin description');
        $this->ps_versions_compliancy = array('min' => '1.7.0', 'max' => _PS_VERSION_);
    }
 
    /**
     * @return boolean
     */
    public function install() {
        if (!parent::install() 
                || !$this->registerHook('actionAdminCustomersListingFieldsModifier')      
        ) {
            return false;
        }
 
        return true;
    }
 
    /**
     * Ajout de champs dans le listing des clients
     * @param type $params
     */
    public function hookActionAdminCustomersListingFieldsModifier($params)
    {
        //Pour l'exemple on va ajouter 2 champs dans le listing
 
 
        // champ simple texte : date d'inscription à la newsletter ( dans la table customer )
        $params['fields']['newsletter_date_add'] = array(
            'title' => $this->l('Newsletter subscribe date')
        );
 
        //Champ un peu plus "complexe" groupe par défaut  select ( id_default_group dans la table customer 
        //et récupération des noms des groupes via une jointure )
        $params['select'] .= ' ,grl.name as default_group_name '; //Ajout du champ dans la requête select
        $params['join'] .= 'LEFT JOIN '._DB_PREFIX_.'group_lang grl ON (a.id_default_group = grl.id_group )'; //Jointure avec la table des groupes
        $groups = Group::getGroups($this->context->language->id); //Récupération de la liste des groupes
 
        $groupsList = array();
        foreach( $groups as $group)
            $groupsList[$group['id_group']] = $group['name'];
 
       //Affichage du nouveau champ ( méthode standard)
        /*$params['fields']['default_group_name'] = array(
            'title' => $this->l('Groupe par défaut'),
            'type' => 'select',
            'list' => $groupsList,
            'filter_key' => 'a!id_default_group' //Clé de filtrage dans la requête sous la forme "NomTable!champ"  
        );*/
 
		//Ajout du nouveau champ dans une certaine position
        $newField = array( 'default_group_name' => array(
            'title' => $this->l('Groupe par défaut'),
            'type' => 'select',
            'list' => $groupsList,
            'filter_key' => 'a!id_default_group' //Clé de filtrage dans la requête sous la forme "NomTable!champ" 
            )
        );
        $this->addNewFieldAfter($newField, 'lastname', $params['fields']);
    }
 
	 /**
     * Ajout du nouveau champ après
     * @param type $fieldParams
     * @param type $afertName
     * @param type $fieldsArray
     */
    public function addNewFieldAfter($fieldParams,$afterName,array &$fieldsArray)
    {
 
        if (array_key_exists($afterName, $fieldsArray)) {
            $pos = array_search($afterName, array_keys($fieldsArray));
            $before = array_slice($fieldsArray,0,$pos+1);
            $after = array_slice($fieldsArray, $pos); 
            $fieldsArray = array_merge($before,$fieldParams,$after);
 
        } else {
            $fieldsArray = array_merge($fieldsArray,$fieldParams);
        }
    }
}
Pas de commentaires

Prestashop 1.7 : Dynamisez vos contenus wysiwyg

La problématique n’est pas nouvelle sur Prestashop, et c’est un point qui me frustre assez souvent et pour lequel j’avais déjà fait un module en 2014 ( cf. https://www.h-hennes.fr/blog/2014/01/18/prestashop-liens-dynamiques-vers-les-pages-cms-dans-lediteur-tinymce/ )
Ce module n’est malheureusement plus fonctionnel sur Prestashop 1.7.

Dans les éditeurs de contenus, il n’est pas possible  des mettre des liens ou des contenus dynamiques.
La bonne nouvelle cependant c’est que la version 1.7 de Prestashop apporte de nouveaux hooks qui vont permettre de modifier ces contenus avant l’affichage des éléments, et donc sans surcharge  🙂

Les éléments dont les contenus sont modifiables sont les suivants :

  • Pages cms
  • Catégories cms
  • Contenu produit
  • Contenu catégorie
  • Contenu Fabriquants
  • Contenu fournisseurs

Pour l’exemple on va partir sur le besoin suivant :

Je souhaite afficher un élément de configuration  dans l’ensemble des contenus noté ci-dessous.
Ce sera la configuration PS_SHOP_EMAIL , qui correspond à l’émail par défaut de la boutique.

Pour afficher cette variable il faudra intégrer dans les zones de contenus le code suivant :

{{configuration name=PS_SHOP_EMAIL}}

Le but du module sera donc de remplacer cet élément par sa valeur de configuration.

Voici le contenu qui sera saisi dans l’admin :

Cms placeholder

Et le résultat :

Cms placeholder result

Vous trouverez ci-dessous le code complet du module qui permets de gérer cela

<?php
class HhContentVars extends Module {
 
    public function __construct() {
        $this->name = 'hhcontentvars';
        $this->tab = 'others';
        $this->author = 'hhennes';
        $this->version = '1.0.0';
        $this->need_instance = 0;
        $this->bootstrap = true;
 
        parent::__construct();
 
        $this->displayName = $this->l('Hh content var');
        $this->description = $this->l('Add content var in wysiwyg editors');
    }
 
    public function install() {
        if (!parent::install() 
                //Hooks Fronts d'affichage
                || !$this->registerHook('filterCmsContent') 
                || !$this->registerHook('filterCmsCategoryContent') 
                || !$this->registerHook('filterProductContent') 
                || !$this->registerHook('filterCategoryContent') 
                || !$this->registerHook('filterManufacturerContent') 
                || !$this->registerHook('filterSupplierContent')
        )
            return false;
 
        return true;
    }
 
 
    /**
     * Filtre des contenus Cms
     * @param $params
     * @return array
     */
    public function hookFilterCmsContent($params) {
 
        $params['object']['content'] = $this->_updateContentVars($params['object']['content']);
 
        return [
            'object' => $params['object']
        ];
    }
 
    /**
     * Filtre des contenus des catégories cms
     * @param $params
     * @return array
     */
    public function hookFilterCmsCategoryContent($params) {
 
        $params['object']['description'] = $this->_updateContentVars($params['object']['description']);
 
        return [
            'object' => $params['object']
        ];
    }
 
    /**
     * Filtre des contenu des produits
     * @param $params
     */
    public function hookFilterProductContent($params) {
        $params['object']['description'] = $this->_updateContentVars($params['object']['description']);
        return [
            'object' => $params['object']
        ];
    }
 
    /**
     * Filtre des contenus des catégories produits
     * @param $params
     * @return array
     */
    public function hookFilterCategoryContent($params) {
 
        $params['object']['description'] = $this->_updateContentVars($params['object']['description']);
 
        return [
            'object' => $params['object']
        ];
    }
 
    /**
     * Filtre des contenus des Marques
     * @param $params
     * @return mixed
     */
    public function hookFilterManufacturerContent($params) {
 
        return $this->_updateContentVars($params['filtered_content']);
    }
 
    /**
     * Filtre des contenus des fournisseurs
     * @param $params
     * @return array
     */
    public function hookFilterSupplierContent($params) {
        $params['object']['description'] = $this->_updateContentVars($params['object']['description']);
 
        return [
            'object' => $params['object']
        ];
    }
 
    /**
     * Mise à jour du contenu
     * @param string : contenu ou il faut remplacer les variables
     * @return string : contenu avec les variables remplacées
     */
    protected function _updateContentVars($content) {
 
        $content = urldecode($content);
 
        //Récupération des éléments de configuration
        preg_match_all('#{{configuration name=(.*)}}#i', $content, $configurations);
        if (isset($configurations[1]) && sizeof($configurations[1])) {
            foreach ($configurations[1] as $conf) {
                if ( $value = Configuration::get($conf) ) {
                    $content = preg_replace('#{{configuration name=' . $conf . '}}#',$value, $content);
                }
            }
        }
 
        return $content;
    }
}

Ceci n’est qu’un exemple, il donc possible de mettre en place autant de tags que souhaités.
Si j’ai le temps durant les prochaines semaines je verrais peu être pour mettre à jour mon module précédent uniquement PS 1.7

2 commentaires

Monitorez vos serveurs avec netdata

J’ai réinstallé mon serveur dédié sous ubuntu 16.04 récement, j’en ai profité donc pour chercher des nouveaux outils de monitoring, et je suis tombé sur « netdata » que j’avais déjà croisé il y’a pas mal de temps mais que je n’avais pas encore installé.

La solution est disponible sur github avec une documentation très complète à l’adresse suivante :
https://github.com/firehol/netdata/wiki/Installation

Il faut reconnaitre que cette solution est réussie graphiquement et très complète ( ça ressemble à un htop très très amélioré )
Nous allons voir comment installer et configurer basiquement cette solution.

Installer via la ligne de commande suivante :

bash <(curl -Ss https://my-netdata.io/kickstart.sh)

( Si curl n’est pas intallé apt-get install curl )

La durée du script d’installation dure environ 5 minutes :

Dans un premier temps il vérifie les prérequis et identifie les librairies manquantes sur votre serveur.
Et vous propose ensuite de les installer.

net data install 1

Vous pouvez ensuite paramétrer l’installation de la solution.
Pour ma part j’ai tout laissé par défaut.

net data install 2

Vous pouvez ensuite accéder à vos statistiques depuis l’ip de votre serveur sur le port 19999, voici une capture d’un des paramètre présent, mais il y’en a des 100 aines disponibles !!

Pour finaliser l’installation vous pouvez rajouter un filtrage ip via la configuration en éditant le fichier
/etc/netdata/netdata.conf et en supprimant le commentaire et en ajoutant votre ip sur la ligne

# allow connections from = localhost

Rechargez ensuite le service pour appliquer ce changement

sudo service netdata restart
Pas de commentaires

Eicaptcha : Ajout d’une configuration avancées des sélecteurs

J’ai eut beaucoup de retours ces dernières semaines sur le non-fonctionnement de mon module de captcha sur les thèmes différents de default-boostrap ( celui par défaut ).
Ceci était du au fait que j’avais stocké directement dans le code des sélecteurs css spécifiques à ce thème.

Vous pouvez le voir sur l’extrait de code suivant :

<script type="text/javascript">
 $(document).ready(function(){
 //Add div where the captcha will be displayed
 $(".submit").before("<div id=\"captcha-box\"></div>"); // <= sélecteur stocké en dur
 
 //Manage form submit 
 $("#submitMessage").click(function(){ // <= sélecteur stocké en dur
 ...
 </script>

La nouvelle version qui sortira dans les prochains jours corrigera ce problème, en permettant d’éditer ces informations directement depuis l’administration.
Dans un nouvel onglet « Paramètres avancés »

captcha-configuration-avancee

Récupération des sélecteurs de votre thème

Attention car la récupération des éléments est assez technique.
Si la notion vous est totalement étrangère je vous invite à consulter la documentation sur ce sujet.

Le plus pratique est d’utiliser les options de développement de votre navigateur  qui s’activent via la touche F12
Une fois ceux-ci activés.

Sélecteur du bouton de soumission du formulaire :
Faites un clic droit sur le bouton de soumission du formulaire et cliquer sur « Examiner l’élément » ( Firefox ) ou « Inspecter » (chrome)
Vous devrez obtenir une capture de ce genre :

Inspecteur bouton formulaire

Dans le cas de cet capture le sélecteur à saisir est #submitMessage car le bouton de soumission a l’id égal à submitMessage.

 

Sélecteur insertion bloc captcha :

Celui-ci est encore un peu plus complexe, car il faut définir l’élément css AVANT LEQUEL nous souhaitons ajouter le bloc de captcha.
Il faut également s’assurer que le bloc sera bien entre les balises <form> et </form> pour que les données soient bien envoyées.

Cela correspond à cette partie du javascript :

$(".submit").before("<div id=\"captcha-box\"></div>");

Dans la configuration par défaut le sélecteur est .submit le bloc du captcha et donc ajouté avant la div « submit » comme vous pouvez voir sur la capture :

Captcha inspecteur container

Pour ceux qui ne sont pas trop à l’aise avec cet élément, il est également possible de rajouter à l’emplacement ou vous souhaitez afficher le captcha directement le code :

<div id="captcha-box"></div>

Pour tous les autres détails de ce module merci de consulter la page spécifique : Module captcha Prestashop

3 commentaires

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
Magento certified developper
Modules Prestashop
Compte Github